mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(server)!: search via typesense (#1778)
* build: add typesense to docker * feat(server): typesense search * feat(web): search * fix(web): show api error response message * chore: search tests * chore: regenerate open api * fix: disable typesense on e2e * fix: number properties for open api (dart) * fix: e2e test * fix: change lat/lng from floats to typesense geopoint * dev: Add smartInfo relation to findAssetById to be able to query against it --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									1cc184ed10
								
							
						
					
					
						commit
						0aaeab124d
					
				@ -17,3 +17,5 @@ ENABLE_MAPBOX=false
 | 
				
			|||||||
# WEB
 | 
					# WEB
 | 
				
			||||||
MAPBOX_KEY=
 | 
					MAPBOX_KEY=
 | 
				
			||||||
VITE_SERVER_ENDPOINT=http://localhost:2283/api
 | 
					VITE_SERVER_ENDPOINT=http://localhost:2283/api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TYPESENSE_ENABLED=false
 | 
				
			||||||
 | 
				
			|||||||
@ -23,6 +23,7 @@ services:
 | 
				
			|||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
      - database
 | 
					      - database
 | 
				
			||||||
 | 
					      - typesense
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  immich-machine-learning:
 | 
					  immich-machine-learning:
 | 
				
			||||||
    container_name: immich_machine_learning
 | 
					    container_name: immich_machine_learning
 | 
				
			||||||
@ -64,6 +65,7 @@ services:
 | 
				
			|||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - database
 | 
					      - database
 | 
				
			||||||
      - immich-server
 | 
					      - immich-server
 | 
				
			||||||
 | 
					      - typesense
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  immich-web:
 | 
					  immich-web:
 | 
				
			||||||
    container_name: immich_web
 | 
					    container_name: immich_web
 | 
				
			||||||
@ -89,6 +91,15 @@ services:
 | 
				
			|||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - immich-server
 | 
					      - immich-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  typesense:
 | 
				
			||||||
 | 
					    container_name: immich_typesense
 | 
				
			||||||
 | 
					    image: typesense/typesense:0.24.0
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
 | 
				
			||||||
 | 
					      - TYPESENSE_DATA_DIR=/data
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tsdata:/data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  redis:
 | 
					  redis:
 | 
				
			||||||
    container_name: immich_redis
 | 
					    container_name: immich_redis
 | 
				
			||||||
    image: redis:6.2
 | 
					    image: redis:6.2
 | 
				
			||||||
@ -129,3 +140,4 @@ services:
 | 
				
			|||||||
volumes:
 | 
					volumes:
 | 
				
			||||||
  pgdata:
 | 
					  pgdata:
 | 
				
			||||||
  model-cache:
 | 
					  model-cache:
 | 
				
			||||||
 | 
					  tsdata:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
version: '3.8'
 | 
					version: "3.8"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
services:
 | 
					services:
 | 
				
			||||||
  immich-server-test:
 | 
					  immich-server-test:
 | 
				
			||||||
@ -9,7 +9,7 @@ services:
 | 
				
			|||||||
      target: builder
 | 
					      target: builder
 | 
				
			||||||
    command: npm run test:e2e
 | 
					    command: npm run test:e2e
 | 
				
			||||||
    expose:
 | 
					    expose:
 | 
				
			||||||
      - '3000'
 | 
					      - "3000"
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ../server:/usr/src/app
 | 
					      - ../server:/usr/src/app
 | 
				
			||||||
      - /usr/src/app/node_modules
 | 
					      - /usr/src/app/node_modules
 | 
				
			||||||
@ -17,6 +17,7 @@ services:
 | 
				
			|||||||
      - .env.test
 | 
					      - .env.test
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - NODE_ENV=development
 | 
					      - NODE_ENV=development
 | 
				
			||||||
 | 
					      - TYPESENSE_ENABLED=false
 | 
				
			||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - immich-redis-test
 | 
					      - immich-redis-test
 | 
				
			||||||
      - immich-database-test
 | 
					      - immich-database-test
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,7 @@ services:
 | 
				
			|||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
      - database
 | 
					      - database
 | 
				
			||||||
 | 
					      - typesense
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  immich-microservices:
 | 
					  immich-microservices:
 | 
				
			||||||
@ -29,6 +30,7 @@ services:
 | 
				
			|||||||
    depends_on:
 | 
					    depends_on:
 | 
				
			||||||
      - redis
 | 
					      - redis
 | 
				
			||||||
      - database
 | 
					      - database
 | 
				
			||||||
 | 
					      - typesense
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  immich-machine-learning:
 | 
					  immich-machine-learning:
 | 
				
			||||||
@ -51,6 +53,15 @@ services:
 | 
				
			|||||||
      - .env
 | 
					      - .env
 | 
				
			||||||
    restart: always
 | 
					    restart: always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  typesense:
 | 
				
			||||||
 | 
					    container_name: immich_typesense
 | 
				
			||||||
 | 
					    image: typesense/typesense:0.24.0
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      - TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
 | 
				
			||||||
 | 
					      - TYPESENSE_DATA_DIR=/data
 | 
				
			||||||
 | 
					    volumes:
 | 
				
			||||||
 | 
					      - tsdata:/data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  redis:
 | 
					  redis:
 | 
				
			||||||
    container_name: immich_redis
 | 
					    container_name: immich_redis
 | 
				
			||||||
    image: redis:6.2
 | 
					    image: redis:6.2
 | 
				
			||||||
@ -88,3 +99,4 @@ services:
 | 
				
			|||||||
volumes:
 | 
					volumes:
 | 
				
			||||||
  pgdata:
 | 
					  pgdata:
 | 
				
			||||||
  model-cache:
 | 
					  model-cache:
 | 
				
			||||||
 | 
					  tsdata:
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,13 @@ REDIS_HOSTNAME=immich_redis
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
 | 
					UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					###################################################################################
 | 
				
			||||||
 | 
					# Typesense
 | 
				
			||||||
 | 
					###################################################################################
 | 
				
			||||||
 | 
					TYPESENSE_API_KEY=some-random-text
 | 
				
			||||||
 | 
					# TYPESENSE_ENABLED=false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
###################################################################################
 | 
					###################################################################################
 | 
				
			||||||
# Reverse Geocoding
 | 
					# Reverse Geocoding
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										21
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							@ -61,7 +61,14 @@ doc/OAuthCallbackDto.md
 | 
				
			|||||||
doc/OAuthConfigDto.md
 | 
					doc/OAuthConfigDto.md
 | 
				
			||||||
doc/OAuthConfigResponseDto.md
 | 
					doc/OAuthConfigResponseDto.md
 | 
				
			||||||
doc/RemoveAssetsDto.md
 | 
					doc/RemoveAssetsDto.md
 | 
				
			||||||
 | 
					doc/SearchAlbumResponseDto.md
 | 
				
			||||||
 | 
					doc/SearchApi.md
 | 
				
			||||||
doc/SearchAssetDto.md
 | 
					doc/SearchAssetDto.md
 | 
				
			||||||
 | 
					doc/SearchAssetResponseDto.md
 | 
				
			||||||
 | 
					doc/SearchConfigResponseDto.md
 | 
				
			||||||
 | 
					doc/SearchFacetCountResponseDto.md
 | 
				
			||||||
 | 
					doc/SearchFacetResponseDto.md
 | 
				
			||||||
 | 
					doc/SearchResponseDto.md
 | 
				
			||||||
doc/ServerInfoApi.md
 | 
					doc/ServerInfoApi.md
 | 
				
			||||||
doc/ServerInfoResponseDto.md
 | 
					doc/ServerInfoResponseDto.md
 | 
				
			||||||
doc/ServerPingResponse.md
 | 
					doc/ServerPingResponse.md
 | 
				
			||||||
@ -103,6 +110,7 @@ lib/api/authentication_api.dart
 | 
				
			|||||||
lib/api/device_info_api.dart
 | 
					lib/api/device_info_api.dart
 | 
				
			||||||
lib/api/job_api.dart
 | 
					lib/api/job_api.dart
 | 
				
			||||||
lib/api/o_auth_api.dart
 | 
					lib/api/o_auth_api.dart
 | 
				
			||||||
 | 
					lib/api/search_api.dart
 | 
				
			||||||
lib/api/server_info_api.dart
 | 
					lib/api/server_info_api.dart
 | 
				
			||||||
lib/api/share_api.dart
 | 
					lib/api/share_api.dart
 | 
				
			||||||
lib/api/system_config_api.dart
 | 
					lib/api/system_config_api.dart
 | 
				
			||||||
@ -167,7 +175,13 @@ lib/model/o_auth_callback_dto.dart
 | 
				
			|||||||
lib/model/o_auth_config_dto.dart
 | 
					lib/model/o_auth_config_dto.dart
 | 
				
			||||||
lib/model/o_auth_config_response_dto.dart
 | 
					lib/model/o_auth_config_response_dto.dart
 | 
				
			||||||
lib/model/remove_assets_dto.dart
 | 
					lib/model/remove_assets_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_album_response_dto.dart
 | 
				
			||||||
lib/model/search_asset_dto.dart
 | 
					lib/model/search_asset_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_asset_response_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_config_response_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_facet_count_response_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_facet_response_dto.dart
 | 
				
			||||||
 | 
					lib/model/search_response_dto.dart
 | 
				
			||||||
lib/model/server_info_response_dto.dart
 | 
					lib/model/server_info_response_dto.dart
 | 
				
			||||||
lib/model/server_ping_response.dart
 | 
					lib/model/server_ping_response.dart
 | 
				
			||||||
lib/model/server_stats_response_dto.dart
 | 
					lib/model/server_stats_response_dto.dart
 | 
				
			||||||
@ -254,7 +268,14 @@ test/o_auth_callback_dto_test.dart
 | 
				
			|||||||
test/o_auth_config_dto_test.dart
 | 
					test/o_auth_config_dto_test.dart
 | 
				
			||||||
test/o_auth_config_response_dto_test.dart
 | 
					test/o_auth_config_response_dto_test.dart
 | 
				
			||||||
test/remove_assets_dto_test.dart
 | 
					test/remove_assets_dto_test.dart
 | 
				
			||||||
 | 
					test/search_album_response_dto_test.dart
 | 
				
			||||||
 | 
					test/search_api_test.dart
 | 
				
			||||||
test/search_asset_dto_test.dart
 | 
					test/search_asset_dto_test.dart
 | 
				
			||||||
 | 
					test/search_asset_response_dto_test.dart
 | 
				
			||||||
 | 
					test/search_config_response_dto_test.dart
 | 
				
			||||||
 | 
					test/search_facet_count_response_dto_test.dart
 | 
				
			||||||
 | 
					test/search_facet_response_dto_test.dart
 | 
				
			||||||
 | 
					test/search_response_dto_test.dart
 | 
				
			||||||
test/server_info_api_test.dart
 | 
					test/server_info_api_test.dart
 | 
				
			||||||
test/server_info_response_dto_test.dart
 | 
					test/server_info_response_dto_test.dart
 | 
				
			||||||
test/server_ping_response_test.dart
 | 
					test/server_ping_response_test.dart
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										8
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							@ -121,6 +121,8 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link | 
 | 
					*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link | 
 | 
				
			||||||
*OAuthApi* | [**mobileRedirect**](doc//OAuthApi.md#mobileredirect) | **GET** /oauth/mobile-redirect | 
 | 
					*OAuthApi* | [**mobileRedirect**](doc//OAuthApi.md#mobileredirect) | **GET** /oauth/mobile-redirect | 
 | 
				
			||||||
*OAuthApi* | [**unlink**](doc//OAuthApi.md#unlink) | **POST** /oauth/unlink | 
 | 
					*OAuthApi* | [**unlink**](doc//OAuthApi.md#unlink) | **POST** /oauth/unlink | 
 | 
				
			||||||
 | 
					*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
				
			||||||
 | 
					*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | 
 | 
				
			||||||
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
					*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | 
 | 
				
			||||||
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 | 
					*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version | 
 | 
				
			||||||
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 | 
					*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats | 
 | 
				
			||||||
@ -204,7 +206,13 @@ Class | Method | HTTP request | Description
 | 
				
			|||||||
 - [OAuthConfigDto](doc//OAuthConfigDto.md)
 | 
					 - [OAuthConfigDto](doc//OAuthConfigDto.md)
 | 
				
			||||||
 - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
 | 
					 - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
 | 
				
			||||||
 - [RemoveAssetsDto](doc//RemoveAssetsDto.md)
 | 
					 - [RemoveAssetsDto](doc//RemoveAssetsDto.md)
 | 
				
			||||||
 | 
					 - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
 | 
				
			||||||
 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
					 - [SearchAssetDto](doc//SearchAssetDto.md)
 | 
				
			||||||
 | 
					 - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
 | 
				
			||||||
 | 
					 - [SearchConfigResponseDto](doc//SearchConfigResponseDto.md)
 | 
				
			||||||
 | 
					 - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
 | 
				
			||||||
 | 
					 - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
 | 
				
			||||||
 | 
					 - [SearchResponseDto](doc//SearchResponseDto.md)
 | 
				
			||||||
 - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
 | 
					 - [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
 | 
				
			||||||
 - [ServerPingResponse](doc//ServerPingResponse.md)
 | 
					 - [ServerPingResponse](doc//ServerPingResponse.md)
 | 
				
			||||||
 - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
 | 
					 - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								mobile/openapi/doc/SearchAlbumResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mobile/openapi/doc/SearchAlbumResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchAlbumResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**total** | **int** |  | 
 | 
				
			||||||
 | 
					**count** | **int** |  | 
 | 
				
			||||||
 | 
					**items** | [**List<AlbumResponseDto>**](AlbumResponseDto.md) |  | [default to const []]
 | 
				
			||||||
 | 
					**facets** | [**List<SearchFacetResponseDto>**](SearchFacetResponseDto.md) |  | [default to const []]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										135
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								mobile/openapi/doc/SearchApi.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					# openapi.api.SearchApi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the API package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All URIs are relative to */api*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Method | HTTP request | Description
 | 
				
			||||||
 | 
					------------- | ------------- | -------------
 | 
				
			||||||
 | 
					[**getSearchConfig**](SearchApi.md#getsearchconfig) | **GET** /search/config | 
 | 
				
			||||||
 | 
					[**search**](SearchApi.md#search) | **GET** /search | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# **getSearchConfig**
 | 
				
			||||||
 | 
					> SearchConfigResponseDto getSearchConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					// TODO Configure HTTP Bearer authorization: bearer
 | 
				
			||||||
 | 
					// Case 1. Use String Token
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
 | 
				
			||||||
 | 
					// Case 2. Use Function which generate token.
 | 
				
			||||||
 | 
					// String yourTokenGeneratorFunction() { ... }
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: cookie
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final api_instance = SearchApi();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.getSearchConfig();
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling SearchApi->getSearchConfig: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					This endpoint does not need any parameter.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**SearchConfigResponseDto**](SearchConfigResponseDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bearer](../README.md#bearer), [cookie](../README.md#cookie)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: Not defined
 | 
				
			||||||
 | 
					 - **Accept**: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# **search**
 | 
				
			||||||
 | 
					> SearchResponseDto search(query, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Example
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					// TODO Configure HTTP Bearer authorization: bearer
 | 
				
			||||||
 | 
					// Case 1. Use String Token
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
 | 
				
			||||||
 | 
					// Case 2. Use Function which generate token.
 | 
				
			||||||
 | 
					// String yourTokenGeneratorFunction() { ... }
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 | 
				
			||||||
 | 
					// TODO Configure API key authorization: cookie
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
 | 
				
			||||||
 | 
					// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
 | 
				
			||||||
 | 
					//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final api_instance = SearchApi();
 | 
				
			||||||
 | 
					final query = query_example; // String | 
 | 
				
			||||||
 | 
					final type = type_example; // String | 
 | 
				
			||||||
 | 
					final isFavorite = true; // bool | 
 | 
				
			||||||
 | 
					final exifInfoPeriodCity = exifInfoPeriodCity_example; // String | 
 | 
				
			||||||
 | 
					final exifInfoPeriodState = exifInfoPeriodState_example; // String | 
 | 
				
			||||||
 | 
					final exifInfoPeriodCountry = exifInfoPeriodCountry_example; // String | 
 | 
				
			||||||
 | 
					final exifInfoPeriodMake = exifInfoPeriodMake_example; // String | 
 | 
				
			||||||
 | 
					final exifInfoPeriodModel = exifInfoPeriodModel_example; // String | 
 | 
				
			||||||
 | 
					final smartInfoPeriodObjects = []; // List<String> | 
 | 
				
			||||||
 | 
					final smartInfoPeriodTags = []; // List<String> | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try {
 | 
				
			||||||
 | 
					    final result = api_instance.search(query, type, isFavorite, exifInfoPeriodCity, exifInfoPeriodState, exifInfoPeriodCountry, exifInfoPeriodMake, exifInfoPeriodModel, smartInfoPeriodObjects, smartInfoPeriodTags);
 | 
				
			||||||
 | 
					    print(result);
 | 
				
			||||||
 | 
					} catch (e) {
 | 
				
			||||||
 | 
					    print('Exception when calling SearchApi->search: $e\n');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Parameters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name | Type | Description  | Notes
 | 
				
			||||||
 | 
					------------- | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					 **query** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **type** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **isFavorite** | **bool**|  | [optional] 
 | 
				
			||||||
 | 
					 **exifInfoPeriodCity** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **exifInfoPeriodState** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **exifInfoPeriodCountry** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **exifInfoPeriodMake** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **exifInfoPeriodModel** | **String**|  | [optional] 
 | 
				
			||||||
 | 
					 **smartInfoPeriodObjects** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
				
			||||||
 | 
					 **smartInfoPeriodTags** | [**List<String>**](String.md)|  | [optional] [default to const []]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Return type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[**SearchResponseDto**](SearchResponseDto.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Authorization
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[bearer](../README.md#bearer), [cookie](../README.md#cookie)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### HTTP request headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - **Content-Type**: Not defined
 | 
				
			||||||
 | 
					 - **Accept**: application/json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										18
									
								
								mobile/openapi/doc/SearchAssetResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mobile/openapi/doc/SearchAssetResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchAssetResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**total** | **int** |  | 
 | 
				
			||||||
 | 
					**count** | **int** |  | 
 | 
				
			||||||
 | 
					**items** | [**List<AssetResponseDto>**](AssetResponseDto.md) |  | [default to const []]
 | 
				
			||||||
 | 
					**facets** | [**List<SearchFacetResponseDto>**](SearchFacetResponseDto.md) |  | [default to const []]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										15
									
								
								mobile/openapi/doc/SearchConfigResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/doc/SearchConfigResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchConfigResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**enabled** | **bool** |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/SearchFacetCountResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/SearchFacetCountResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchFacetCountResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**count** | **int** |  | 
 | 
				
			||||||
 | 
					**value** | **String** |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/SearchFacetResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/SearchFacetResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchFacetResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**fieldName** | **String** |  | 
 | 
				
			||||||
 | 
					**counts** | [**List<SearchFacetCountResponseDto>**](SearchFacetCountResponseDto.md) |  | [default to const []]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/SearchResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/SearchResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# openapi.model.SearchResponseDto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Load the model package
 | 
				
			||||||
 | 
					```dart
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Properties
 | 
				
			||||||
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
 | 
					**albums** | [**SearchAlbumResponseDto**](SearchAlbumResponseDto.md) |  | 
 | 
				
			||||||
 | 
					**assets** | [**SearchAssetResponseDto**](SearchAssetResponseDto.md) |  | 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										7
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							@ -35,6 +35,7 @@ part 'api/authentication_api.dart';
 | 
				
			|||||||
part 'api/device_info_api.dart';
 | 
					part 'api/device_info_api.dart';
 | 
				
			||||||
part 'api/job_api.dart';
 | 
					part 'api/job_api.dart';
 | 
				
			||||||
part 'api/o_auth_api.dart';
 | 
					part 'api/o_auth_api.dart';
 | 
				
			||||||
 | 
					part 'api/search_api.dart';
 | 
				
			||||||
part 'api/server_info_api.dart';
 | 
					part 'api/server_info_api.dart';
 | 
				
			||||||
part 'api/share_api.dart';
 | 
					part 'api/share_api.dart';
 | 
				
			||||||
part 'api/system_config_api.dart';
 | 
					part 'api/system_config_api.dart';
 | 
				
			||||||
@ -92,7 +93,13 @@ part 'model/o_auth_callback_dto.dart';
 | 
				
			|||||||
part 'model/o_auth_config_dto.dart';
 | 
					part 'model/o_auth_config_dto.dart';
 | 
				
			||||||
part 'model/o_auth_config_response_dto.dart';
 | 
					part 'model/o_auth_config_response_dto.dart';
 | 
				
			||||||
part 'model/remove_assets_dto.dart';
 | 
					part 'model/remove_assets_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_album_response_dto.dart';
 | 
				
			||||||
part 'model/search_asset_dto.dart';
 | 
					part 'model/search_asset_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_asset_response_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_config_response_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_facet_count_response_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_facet_response_dto.dart';
 | 
				
			||||||
 | 
					part 'model/search_response_dto.dart';
 | 
				
			||||||
part 'model/server_info_response_dto.dart';
 | 
					part 'model/server_info_response_dto.dart';
 | 
				
			||||||
part 'model/server_ping_response.dart';
 | 
					part 'model/server_ping_response.dart';
 | 
				
			||||||
part 'model/server_stats_response_dto.dart';
 | 
					part 'model/server_stats_response_dto.dart';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										181
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,181 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchApi {
 | 
				
			||||||
 | 
					  SearchApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final ApiClient apiClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// 
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Note: This method returns the HTTP [Response].
 | 
				
			||||||
 | 
					  Future<Response> getSearchConfigWithHttpInfo() async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/search/config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'GET',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// 
 | 
				
			||||||
 | 
					  Future<SearchConfigResponseDto?> getSearchConfig() async {
 | 
				
			||||||
 | 
					    final response = await getSearchConfigWithHttpInfo();
 | 
				
			||||||
 | 
					    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), 'SearchConfigResponseDto',) as SearchConfigResponseDto;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// 
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Note: This method returns the HTTP [Response].
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] query:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] type:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [bool] isFavorite:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodCity:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodState:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodCountry:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodMake:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodModel:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [List<String>] smartInfoPeriodObjects:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [List<String>] smartInfoPeriodTags:
 | 
				
			||||||
 | 
					  Future<Response> searchWithHttpInfo({ String? query, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, }) async {
 | 
				
			||||||
 | 
					    // ignore: prefer_const_declarations
 | 
				
			||||||
 | 
					    final path = r'/search';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ignore: prefer_final_locals
 | 
				
			||||||
 | 
					    Object? postBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final queryParams = <QueryParam>[];
 | 
				
			||||||
 | 
					    final headerParams = <String, String>{};
 | 
				
			||||||
 | 
					    final formParams = <String, String>{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (query != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'query', query));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (type != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'type', type));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (isFavorite != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (exifInfoPeriodCity != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'exifInfo.city', exifInfoPeriodCity));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (exifInfoPeriodState != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'exifInfo.state', exifInfoPeriodState));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (exifInfoPeriodCountry != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'exifInfo.country', exifInfoPeriodCountry));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (exifInfoPeriodMake != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'exifInfo.make', exifInfoPeriodMake));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (exifInfoPeriodModel != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('', 'exifInfo.model', exifInfoPeriodModel));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (smartInfoPeriodObjects != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('multi', 'smartInfo.objects', smartInfoPeriodObjects));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (smartInfoPeriodTags != null) {
 | 
				
			||||||
 | 
					      queryParams.addAll(_queryParams('multi', 'smartInfo.tags', smartInfoPeriodTags));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentTypes = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return apiClient.invokeAPI(
 | 
				
			||||||
 | 
					      path,
 | 
				
			||||||
 | 
					      'GET',
 | 
				
			||||||
 | 
					      queryParams,
 | 
				
			||||||
 | 
					      postBody,
 | 
				
			||||||
 | 
					      headerParams,
 | 
				
			||||||
 | 
					      formParams,
 | 
				
			||||||
 | 
					      contentTypes.isEmpty ? null : contentTypes.first,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// 
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Parameters:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] query:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] type:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [bool] isFavorite:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodCity:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodState:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodCountry:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodMake:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [String] exifInfoPeriodModel:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [List<String>] smartInfoPeriodObjects:
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  /// * [List<String>] smartInfoPeriodTags:
 | 
				
			||||||
 | 
					  Future<SearchResponseDto?> search({ String? query, String? type, bool? isFavorite, String? exifInfoPeriodCity, String? exifInfoPeriodState, String? exifInfoPeriodCountry, String? exifInfoPeriodMake, String? exifInfoPeriodModel, List<String>? smartInfoPeriodObjects, List<String>? smartInfoPeriodTags, }) async {
 | 
				
			||||||
 | 
					    final response = await searchWithHttpInfo( query: query, type: type, isFavorite: isFavorite, exifInfoPeriodCity: exifInfoPeriodCity, exifInfoPeriodState: exifInfoPeriodState, exifInfoPeriodCountry: exifInfoPeriodCountry, exifInfoPeriodMake: exifInfoPeriodMake, exifInfoPeriodModel: exifInfoPeriodModel, smartInfoPeriodObjects: smartInfoPeriodObjects, smartInfoPeriodTags: smartInfoPeriodTags, );
 | 
				
			||||||
 | 
					    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), 'SearchResponseDto',) as SearchResponseDto;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							@ -294,8 +294,20 @@ class ApiClient {
 | 
				
			|||||||
          return OAuthConfigResponseDto.fromJson(value);
 | 
					          return OAuthConfigResponseDto.fromJson(value);
 | 
				
			||||||
        case 'RemoveAssetsDto':
 | 
					        case 'RemoveAssetsDto':
 | 
				
			||||||
          return RemoveAssetsDto.fromJson(value);
 | 
					          return RemoveAssetsDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchAlbumResponseDto':
 | 
				
			||||||
 | 
					          return SearchAlbumResponseDto.fromJson(value);
 | 
				
			||||||
        case 'SearchAssetDto':
 | 
					        case 'SearchAssetDto':
 | 
				
			||||||
          return SearchAssetDto.fromJson(value);
 | 
					          return SearchAssetDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchAssetResponseDto':
 | 
				
			||||||
 | 
					          return SearchAssetResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchConfigResponseDto':
 | 
				
			||||||
 | 
					          return SearchConfigResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchFacetCountResponseDto':
 | 
				
			||||||
 | 
					          return SearchFacetCountResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchFacetResponseDto':
 | 
				
			||||||
 | 
					          return SearchFacetResponseDto.fromJson(value);
 | 
				
			||||||
 | 
					        case 'SearchResponseDto':
 | 
				
			||||||
 | 
					          return SearchResponseDto.fromJson(value);
 | 
				
			||||||
        case 'ServerInfoResponseDto':
 | 
					        case 'ServerInfoResponseDto':
 | 
				
			||||||
          return ServerInfoResponseDto.fromJson(value);
 | 
					          return ServerInfoResponseDto.fromJson(value);
 | 
				
			||||||
        case 'ServerPingResponse':
 | 
					        case 'ServerPingResponse':
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										135
									
								
								mobile/openapi/lib/model/search_album_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								mobile/openapi/lib/model/search_album_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchAlbumResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchAlbumResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchAlbumResponseDto({
 | 
				
			||||||
 | 
					    required this.total,
 | 
				
			||||||
 | 
					    required this.count,
 | 
				
			||||||
 | 
					    this.items = const [],
 | 
				
			||||||
 | 
					    this.facets = const [],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int total;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<AlbumResponseDto> items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<SearchFacetResponseDto> facets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchAlbumResponseDto &&
 | 
				
			||||||
 | 
					     other.total == total &&
 | 
				
			||||||
 | 
					     other.count == count &&
 | 
				
			||||||
 | 
					     other.items == items &&
 | 
				
			||||||
 | 
					     other.facets == facets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (total.hashCode) +
 | 
				
			||||||
 | 
					    (count.hashCode) +
 | 
				
			||||||
 | 
					    (items.hashCode) +
 | 
				
			||||||
 | 
					    (facets.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchAlbumResponseDto[total=$total, count=$count, items=$items, facets=$facets]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'total'] = this.total;
 | 
				
			||||||
 | 
					      json[r'count'] = this.count;
 | 
				
			||||||
 | 
					      json[r'items'] = this.items;
 | 
				
			||||||
 | 
					      json[r'facets'] = this.facets;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchAlbumResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchAlbumResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchAlbumResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchAlbumResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchAlbumResponseDto(
 | 
				
			||||||
 | 
					        total: mapValueOfType<int>(json, r'total')!,
 | 
				
			||||||
 | 
					        count: mapValueOfType<int>(json, r'count')!,
 | 
				
			||||||
 | 
					        items: AlbumResponseDto.listFromJson(json[r'items'])!,
 | 
				
			||||||
 | 
					        facets: SearchFacetResponseDto.listFromJson(json[r'facets'])!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchAlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchAlbumResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchAlbumResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchAlbumResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchAlbumResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchAlbumResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchAlbumResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchAlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchAlbumResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchAlbumResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'total',
 | 
				
			||||||
 | 
					    'count',
 | 
				
			||||||
 | 
					    'items',
 | 
				
			||||||
 | 
					    'facets',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										135
									
								
								mobile/openapi/lib/model/search_asset_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								mobile/openapi/lib/model/search_asset_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchAssetResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchAssetResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchAssetResponseDto({
 | 
				
			||||||
 | 
					    required this.total,
 | 
				
			||||||
 | 
					    required this.count,
 | 
				
			||||||
 | 
					    this.items = const [],
 | 
				
			||||||
 | 
					    this.facets = const [],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int total;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<AssetResponseDto> items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<SearchFacetResponseDto> facets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchAssetResponseDto &&
 | 
				
			||||||
 | 
					     other.total == total &&
 | 
				
			||||||
 | 
					     other.count == count &&
 | 
				
			||||||
 | 
					     other.items == items &&
 | 
				
			||||||
 | 
					     other.facets == facets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (total.hashCode) +
 | 
				
			||||||
 | 
					    (count.hashCode) +
 | 
				
			||||||
 | 
					    (items.hashCode) +
 | 
				
			||||||
 | 
					    (facets.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchAssetResponseDto[total=$total, count=$count, items=$items, facets=$facets]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'total'] = this.total;
 | 
				
			||||||
 | 
					      json[r'count'] = this.count;
 | 
				
			||||||
 | 
					      json[r'items'] = this.items;
 | 
				
			||||||
 | 
					      json[r'facets'] = this.facets;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchAssetResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchAssetResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchAssetResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchAssetResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchAssetResponseDto(
 | 
				
			||||||
 | 
					        total: mapValueOfType<int>(json, r'total')!,
 | 
				
			||||||
 | 
					        count: mapValueOfType<int>(json, r'count')!,
 | 
				
			||||||
 | 
					        items: AssetResponseDto.listFromJson(json[r'items'])!,
 | 
				
			||||||
 | 
					        facets: SearchFacetResponseDto.listFromJson(json[r'facets'])!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchAssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchAssetResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchAssetResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchAssetResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchAssetResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchAssetResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchAssetResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchAssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchAssetResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchAssetResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'total',
 | 
				
			||||||
 | 
					    'count',
 | 
				
			||||||
 | 
					    'items',
 | 
				
			||||||
 | 
					    'facets',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										111
									
								
								mobile/openapi/lib/model/search_config_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								mobile/openapi/lib/model/search_config_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,111 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchConfigResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchConfigResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchConfigResponseDto({
 | 
				
			||||||
 | 
					    required this.enabled,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool enabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchConfigResponseDto &&
 | 
				
			||||||
 | 
					     other.enabled == enabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (enabled.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchConfigResponseDto[enabled=$enabled]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'enabled'] = this.enabled;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchConfigResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchConfigResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchConfigResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchConfigResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchConfigResponseDto(
 | 
				
			||||||
 | 
					        enabled: mapValueOfType<bool>(json, r'enabled')!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchConfigResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchConfigResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchConfigResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchConfigResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchConfigResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchConfigResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchConfigResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchConfigResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchConfigResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchConfigResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'enabled',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/search_facet_count_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/search_facet_count_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchFacetCountResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchFacetCountResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchFacetCountResponseDto({
 | 
				
			||||||
 | 
					    required this.count,
 | 
				
			||||||
 | 
					    required this.value,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  int count;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchFacetCountResponseDto &&
 | 
				
			||||||
 | 
					     other.count == count &&
 | 
				
			||||||
 | 
					     other.value == value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (count.hashCode) +
 | 
				
			||||||
 | 
					    (value.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchFacetCountResponseDto[count=$count, value=$value]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'count'] = this.count;
 | 
				
			||||||
 | 
					      json[r'value'] = this.value;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchFacetCountResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchFacetCountResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchFacetCountResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchFacetCountResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchFacetCountResponseDto(
 | 
				
			||||||
 | 
					        count: mapValueOfType<int>(json, r'count')!,
 | 
				
			||||||
 | 
					        value: mapValueOfType<String>(json, r'value')!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchFacetCountResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchFacetCountResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchFacetCountResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchFacetCountResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchFacetCountResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchFacetCountResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchFacetCountResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchFacetCountResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchFacetCountResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchFacetCountResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'count',
 | 
				
			||||||
 | 
					    'value',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/search_facet_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/search_facet_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchFacetResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchFacetResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchFacetResponseDto({
 | 
				
			||||||
 | 
					    required this.fieldName,
 | 
				
			||||||
 | 
					    this.counts = const [],
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String fieldName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<SearchFacetCountResponseDto> counts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchFacetResponseDto &&
 | 
				
			||||||
 | 
					     other.fieldName == fieldName &&
 | 
				
			||||||
 | 
					     other.counts == counts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (fieldName.hashCode) +
 | 
				
			||||||
 | 
					    (counts.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchFacetResponseDto[fieldName=$fieldName, counts=$counts]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'fieldName'] = this.fieldName;
 | 
				
			||||||
 | 
					      json[r'counts'] = this.counts;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchFacetResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchFacetResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchFacetResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchFacetResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchFacetResponseDto(
 | 
				
			||||||
 | 
					        fieldName: mapValueOfType<String>(json, r'fieldName')!,
 | 
				
			||||||
 | 
					        counts: SearchFacetCountResponseDto.listFromJson(json[r'counts'])!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchFacetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchFacetResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchFacetResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchFacetResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchFacetResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchFacetResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchFacetResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchFacetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchFacetResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchFacetResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'fieldName',
 | 
				
			||||||
 | 
					    'counts',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										119
									
								
								mobile/openapi/lib/model/search_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								mobile/openapi/lib/model/search_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 SearchResponseDto {
 | 
				
			||||||
 | 
					  /// Returns a new [SearchResponseDto] instance.
 | 
				
			||||||
 | 
					  SearchResponseDto({
 | 
				
			||||||
 | 
					    required this.albums,
 | 
				
			||||||
 | 
					    required this.assets,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SearchAlbumResponseDto albums;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SearchAssetResponseDto assets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) => identical(this, other) || other is SearchResponseDto &&
 | 
				
			||||||
 | 
					     other.albums == albums &&
 | 
				
			||||||
 | 
					     other.assets == assets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode =>
 | 
				
			||||||
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
 | 
					    (albums.hashCode) +
 | 
				
			||||||
 | 
					    (assets.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() => 'SearchResponseDto[albums=$albums, assets=$assets]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
 | 
					      json[r'albums'] = this.albums;
 | 
				
			||||||
 | 
					      json[r'assets'] = this.assets;
 | 
				
			||||||
 | 
					    return json;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Returns a new [SearchResponseDto] instance and imports its values from
 | 
				
			||||||
 | 
					  /// [value] if it's a [Map], null otherwise.
 | 
				
			||||||
 | 
					  // ignore: prefer_constructors_over_static_methods
 | 
				
			||||||
 | 
					  static SearchResponseDto? fromJson(dynamic value) {
 | 
				
			||||||
 | 
					    if (value is Map) {
 | 
				
			||||||
 | 
					      final json = value.cast<String, dynamic>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Ensure that the map contains the required keys.
 | 
				
			||||||
 | 
					      // Note 1: the values aren't checked for validity beyond being non-null.
 | 
				
			||||||
 | 
					      // Note 2: this code is stripped in release mode!
 | 
				
			||||||
 | 
					      assert(() {
 | 
				
			||||||
 | 
					        requiredKeys.forEach((key) {
 | 
				
			||||||
 | 
					          assert(json.containsKey(key), 'Required key "SearchResponseDto[$key]" is missing from JSON.');
 | 
				
			||||||
 | 
					          assert(json[key] != null, 'Required key "SearchResponseDto[$key]" has a null value in JSON.');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return SearchResponseDto(
 | 
				
			||||||
 | 
					        albums: SearchAlbumResponseDto.fromJson(json[r'albums'])!,
 | 
				
			||||||
 | 
					        assets: SearchAssetResponseDto.fromJson(json[r'assets'])!,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static List<SearchResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final result = <SearchResponseDto>[];
 | 
				
			||||||
 | 
					    if (json is List && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      for (final row in json) {
 | 
				
			||||||
 | 
					        final value = SearchResponseDto.fromJson(row);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          result.add(value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result.toList(growable: growable);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static Map<String, SearchResponseDto> mapFromJson(dynamic json) {
 | 
				
			||||||
 | 
					    final map = <String, SearchResponseDto>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchResponseDto.fromJson(entry.value);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // maps a json object with a list of SearchResponseDto-objects as value to a dart map
 | 
				
			||||||
 | 
					  static Map<String, List<SearchResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
 | 
				
			||||||
 | 
					    final map = <String, List<SearchResponseDto>>{};
 | 
				
			||||||
 | 
					    if (json is Map && json.isNotEmpty) {
 | 
				
			||||||
 | 
					      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
 | 
				
			||||||
 | 
					      for (final entry in json.entries) {
 | 
				
			||||||
 | 
					        final value = SearchResponseDto.listFromJson(entry.value, growable: growable,);
 | 
				
			||||||
 | 
					        if (value != null) {
 | 
				
			||||||
 | 
					          map[entry.key] = value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return map;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
 | 
					    'albums',
 | 
				
			||||||
 | 
					    'assets',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										42
									
								
								mobile/openapi/test/search_album_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								mobile/openapi/test/search_album_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchAlbumResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchAlbumResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchAlbumResponseDto', () {
 | 
				
			||||||
 | 
					    // int total
 | 
				
			||||||
 | 
					    test('to test the property `total`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int count
 | 
				
			||||||
 | 
					    test('to test the property `count`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<AlbumResponseDto> items (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `items`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<SearchFacetResponseDto> facets (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `facets`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								mobile/openapi/test/search_api_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// tests for SearchApi
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchApi();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('tests for SearchApi', () {
 | 
				
			||||||
 | 
					    // 
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    //Future<SearchConfigResponseDto> getSearchConfig() async
 | 
				
			||||||
 | 
					    test('test getSearchConfig', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    //Future<SearchResponseDto> search({ String query, String type, bool isFavorite, String exifInfoPeriodCity, String exifInfoPeriodState, String exifInfoPeriodCountry, String exifInfoPeriodMake, String exifInfoPeriodModel, List<String> smartInfoPeriodObjects, List<String> smartInfoPeriodTags }) async
 | 
				
			||||||
 | 
					    test('test search', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										42
									
								
								mobile/openapi/test/search_asset_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								mobile/openapi/test/search_asset_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchAssetResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchAssetResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchAssetResponseDto', () {
 | 
				
			||||||
 | 
					    // int total
 | 
				
			||||||
 | 
					    test('to test the property `total`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // int count
 | 
				
			||||||
 | 
					    test('to test the property `count`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<AssetResponseDto> items (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `items`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<SearchFacetResponseDto> facets (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `facets`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								mobile/openapi/test/search_config_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								mobile/openapi/test/search_config_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchConfigResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchConfigResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchConfigResponseDto', () {
 | 
				
			||||||
 | 
					    // bool enabled
 | 
				
			||||||
 | 
					    test('to test the property `enabled`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								mobile/openapi/test/search_facet_count_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/search_facet_count_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchFacetCountResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchFacetCountResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchFacetCountResponseDto', () {
 | 
				
			||||||
 | 
					    // int count
 | 
				
			||||||
 | 
					    test('to test the property `count`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // String value
 | 
				
			||||||
 | 
					    test('to test the property `value`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								mobile/openapi/test/search_facet_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/search_facet_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchFacetResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchFacetResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchFacetResponseDto', () {
 | 
				
			||||||
 | 
					    // String fieldName
 | 
				
			||||||
 | 
					    test('to test the property `fieldName`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // List<SearchFacetCountResponseDto> counts (default value: const [])
 | 
				
			||||||
 | 
					    test('to test the property `counts`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								mobile/openapi/test/search_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/search_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// AUTO-GENERATED FILE, DO NOT MODIFY!
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// @dart=2.12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					import 'package:test/test.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tests for SearchResponseDto
 | 
				
			||||||
 | 
					void main() {
 | 
				
			||||||
 | 
					  // final instance = SearchResponseDto();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  group('test SearchResponseDto', () {
 | 
				
			||||||
 | 
					    // SearchAlbumResponseDto albums
 | 
				
			||||||
 | 
					    test('to test the property `albums`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // SearchAssetResponseDto assets
 | 
				
			||||||
 | 
					    test('to test the property `assets`', () async {
 | 
				
			||||||
 | 
					      // TODO
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,7 +2,7 @@ import { AlbumService } from './album.service';
 | 
				
			|||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
					import { AuthUserDto } from '../../decorators/auth-user.decorator';
 | 
				
			||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
 | 
					import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
 | 
				
			||||||
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra';
 | 
					import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra';
 | 
				
			||||||
import { AlbumResponseDto, ICryptoRepository, mapUser } from '@app/domain';
 | 
					import { AlbumResponseDto, ICryptoRepository, IJobRepository, JobName, mapUser } from '@app/domain';
 | 
				
			||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
					import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			||||||
import { IAlbumRepository } from './album-repository';
 | 
					import { IAlbumRepository } from './album-repository';
 | 
				
			||||||
import { DownloadService } from '../../modules/download/download.service';
 | 
					import { DownloadService } from '../../modules/download/download.service';
 | 
				
			||||||
@ -10,6 +10,7 @@ import { ISharedLinkRepository } from '@app/domain';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  assetEntityStub,
 | 
					  assetEntityStub,
 | 
				
			||||||
  newCryptoRepositoryMock,
 | 
					  newCryptoRepositoryMock,
 | 
				
			||||||
 | 
					  newJobRepositoryMock,
 | 
				
			||||||
  newSharedLinkRepositoryMock,
 | 
					  newSharedLinkRepositoryMock,
 | 
				
			||||||
  userEntityStub,
 | 
					  userEntityStub,
 | 
				
			||||||
} from '@app/domain/../test';
 | 
					} from '@app/domain/../test';
 | 
				
			||||||
@ -20,6 +21,7 @@ describe('Album service', () => {
 | 
				
			|||||||
  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
 | 
					  let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
 | 
				
			||||||
  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
					  let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
 | 
				
			||||||
  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
					  let cryptoMock: jest.Mocked<ICryptoRepository>;
 | 
				
			||||||
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const authUser: AuthUserDto = Object.freeze({
 | 
					  const authUser: AuthUserDto = Object.freeze({
 | 
				
			||||||
    id: '1111',
 | 
					    id: '1111',
 | 
				
			||||||
@ -139,12 +141,14 @@ describe('Album service', () => {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					    cryptoMock = newCryptoRepositoryMock();
 | 
				
			||||||
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sut = new AlbumService(
 | 
					    sut = new AlbumService(
 | 
				
			||||||
      albumRepositoryMock,
 | 
					      albumRepositoryMock,
 | 
				
			||||||
      sharedLinkRepositoryMock,
 | 
					      sharedLinkRepositoryMock,
 | 
				
			||||||
      downloadServiceMock as DownloadService,
 | 
					      downloadServiceMock as DownloadService,
 | 
				
			||||||
      cryptoMock,
 | 
					      cryptoMock,
 | 
				
			||||||
 | 
					      jobMock,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -158,6 +162,7 @@ describe('Album service', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    expect(result.id).toEqual(albumEntity.id);
 | 
					    expect(result.id).toEqual(albumEntity.id);
 | 
				
			||||||
    expect(result.albumName).toEqual(albumEntity.albumName);
 | 
					    expect(result.albumName).toEqual(albumEntity.albumName);
 | 
				
			||||||
 | 
					    expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEARCH_INDEX_ALBUM, data: { album: albumEntity } });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('gets list of albums for auth user', async () => {
 | 
					  it('gets list of albums for auth user', async () => {
 | 
				
			||||||
@ -291,9 +296,8 @@ describe('Album service', () => {
 | 
				
			|||||||
    const updatedAlbumName = 'new album name';
 | 
					    const updatedAlbumName = 'new album name';
 | 
				
			||||||
    const updatedAlbumThumbnailAssetId = '69d2f917-0b31-48d8-9d7d-673b523f1aac';
 | 
					    const updatedAlbumThumbnailAssetId = '69d2f917-0b31-48d8-9d7d-673b523f1aac';
 | 
				
			||||||
    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
					    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
    albumRepositoryMock.updateAlbum.mockImplementation(() =>
 | 
					    const updatedAlbum = { ...albumEntity, albumName: updatedAlbumName };
 | 
				
			||||||
      Promise.resolve<AlbumEntity>({ ...albumEntity, albumName: updatedAlbumName }),
 | 
					    albumRepositoryMock.updateAlbum.mockResolvedValue(updatedAlbum);
 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = await sut.updateAlbumInfo(
 | 
					    const result = await sut.updateAlbumInfo(
 | 
				
			||||||
      authUser,
 | 
					      authUser,
 | 
				
			||||||
@ -311,6 +315,7 @@ describe('Album service', () => {
 | 
				
			|||||||
      albumName: updatedAlbumName,
 | 
					      albumName: updatedAlbumName,
 | 
				
			||||||
      albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
 | 
					      albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEARCH_INDEX_ALBUM, data: { album: updatedAlbum } });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('prevents updating a not owned album (shared with auth user)', async () => {
 | 
					  it('prevents updating a not owned album (shared with auth user)', async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import { AddUsersDto } from './dto/add-users.dto';
 | 
				
			|||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
 | 
					import { RemoveAssetsDto } from './dto/remove-assets.dto';
 | 
				
			||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
 | 
					import { UpdateAlbumDto } from './dto/update-album.dto';
 | 
				
			||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
 | 
					import { GetAlbumsDto } from './dto/get-albums.dto';
 | 
				
			||||||
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from '@app/domain';
 | 
					import { AlbumResponseDto, IJobRepository, JobName, mapAlbum, mapAlbumExcludeAssetInfo } from '@app/domain';
 | 
				
			||||||
import { IAlbumRepository } from './album-repository';
 | 
					import { IAlbumRepository } from './album-repository';
 | 
				
			||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
 | 
					import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
 | 
				
			||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
					import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
 | 
				
			||||||
@ -27,6 +27,7 @@ export class AlbumService {
 | 
				
			|||||||
    @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
 | 
					    @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
 | 
				
			||||||
    private downloadService: DownloadService,
 | 
					    private downloadService: DownloadService,
 | 
				
			||||||
    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
 | 
					    @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
 | 
				
			||||||
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
 | 
					    this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -56,6 +57,7 @@ export class AlbumService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async create(authUser: AuthUserDto, createAlbumDto: CreateAlbumDto): Promise<AlbumResponseDto> {
 | 
					  async create(authUser: AuthUserDto, createAlbumDto: CreateAlbumDto): Promise<AlbumResponseDto> {
 | 
				
			||||||
    const albumEntity = await this.albumRepository.create(authUser.id, createAlbumDto);
 | 
					    const albumEntity = await this.albumRepository.create(authUser.id, createAlbumDto);
 | 
				
			||||||
 | 
					    await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { album: albumEntity } });
 | 
				
			||||||
    return mapAlbum(albumEntity);
 | 
					    return mapAlbum(albumEntity);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -105,6 +107,7 @@ export class AlbumService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.albumRepository.delete(album);
 | 
					    await this.albumRepository.delete(album);
 | 
				
			||||||
 | 
					    await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ALBUM, data: { id: albumId } });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async removeUserFromAlbum(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
 | 
					  async removeUserFromAlbum(authUser: AuthUserDto, albumId: string, userId: string | 'me'): Promise<void> {
 | 
				
			||||||
@ -171,6 +174,9 @@ export class AlbumService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updatedAlbum = await this.albumRepository.updateAlbum(album, updateAlbumDto);
 | 
					    const updatedAlbum = await this.albumRepository.updateAlbum(album, updateAlbumDto);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { album: updatedAlbum } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return mapAlbum(updatedAlbum);
 | 
					    return mapAlbum(updatedAlbum);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -252,7 +252,7 @@ export class AssetRepository implements IAssetRepository {
 | 
				
			|||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        id: assetId,
 | 
					        id: assetId,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      relations: ['exifInfo', 'tags', 'sharedLinks'],
 | 
					      relations: ['exifInfo', 'tags', 'sharedLinks', 'smartInfo'],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -445,6 +445,8 @@ describe('AssetService', () => {
 | 
				
			|||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(jobMock.queue.mock.calls).toEqual([
 | 
					      expect(jobMock.queue.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [{ name: JobName.SEARCH_REMOVE_ASSET, data: { id: 'asset1' } }],
 | 
				
			||||||
 | 
					        [{ name: JobName.SEARCH_REMOVE_ASSET, data: { id: 'asset2' } }],
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            name: JobName.DELETE_FILES,
 | 
					            name: JobName.DELETE_FILES,
 | 
				
			||||||
 | 
				
			|||||||
@ -170,6 +170,8 @@ export class AssetService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const updatedAsset = await this._assetRepository.update(authUser.id, asset, dto);
 | 
					    const updatedAsset = await this._assetRepository.update(authUser.id, asset, dto);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { asset: updatedAsset } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return mapAsset(updatedAsset);
 | 
					    return mapAsset(updatedAsset);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -425,6 +427,7 @@ export class AssetService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await this._assetRepository.remove(asset);
 | 
					        await this._assetRepository.remove(asset);
 | 
				
			||||||
 | 
					        await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { id } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
 | 
					        result.push({ id, status: DeleteAssetStatusEnum.SUCCESS });
 | 
				
			||||||
        deleteQueue.push(asset.originalPath, asset.webpPath, asset.resizePath);
 | 
					        deleteQueue.push(asset.originalPath, asset.webpPath, asset.resizePath);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { immichAppConfig } from '@app/common/config';
 | 
					import { immichAppConfig } from '@app/common/config';
 | 
				
			||||||
import { Module } from '@nestjs/common';
 | 
					import { Module, OnModuleInit } from '@nestjs/common';
 | 
				
			||||||
import { AssetModule } from './api-v1/asset/asset.module';
 | 
					import { AssetModule } from './api-v1/asset/asset.module';
 | 
				
			||||||
import { ConfigModule } from '@nestjs/config';
 | 
					import { ConfigModule } from '@nestjs/config';
 | 
				
			||||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
 | 
					import { ServerInfoModule } from './api-v1/server-info/server-info.module';
 | 
				
			||||||
@ -9,13 +9,14 @@ import { ScheduleModule } from '@nestjs/schedule';
 | 
				
			|||||||
import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
 | 
					import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module';
 | 
				
			||||||
import { JobModule } from './api-v1/job/job.module';
 | 
					import { JobModule } from './api-v1/job/job.module';
 | 
				
			||||||
import { TagModule } from './api-v1/tag/tag.module';
 | 
					import { TagModule } from './api-v1/tag/tag.module';
 | 
				
			||||||
import { DomainModule } from '@app/domain';
 | 
					import { DomainModule, SearchService } from '@app/domain';
 | 
				
			||||||
import { InfraModule } from '@app/infra';
 | 
					import { InfraModule } from '@app/infra';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  APIKeyController,
 | 
					  APIKeyController,
 | 
				
			||||||
  AuthController,
 | 
					  AuthController,
 | 
				
			||||||
  DeviceInfoController,
 | 
					  DeviceInfoController,
 | 
				
			||||||
  OAuthController,
 | 
					  OAuthController,
 | 
				
			||||||
 | 
					  SearchController,
 | 
				
			||||||
  ShareController,
 | 
					  ShareController,
 | 
				
			||||||
  SystemConfigController,
 | 
					  SystemConfigController,
 | 
				
			||||||
  UserController,
 | 
					  UserController,
 | 
				
			||||||
@ -46,16 +47,21 @@ import { AuthGuard } from './middlewares/auth.guard';
 | 
				
			|||||||
    TagModule,
 | 
					    TagModule,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  controllers: [
 | 
					  controllers: [
 | 
				
			||||||
    //
 | 
					 | 
				
			||||||
    AppController,
 | 
					    AppController,
 | 
				
			||||||
    APIKeyController,
 | 
					    APIKeyController,
 | 
				
			||||||
    AuthController,
 | 
					    AuthController,
 | 
				
			||||||
    DeviceInfoController,
 | 
					    DeviceInfoController,
 | 
				
			||||||
    OAuthController,
 | 
					    OAuthController,
 | 
				
			||||||
 | 
					    SearchController,
 | 
				
			||||||
    ShareController,
 | 
					    ShareController,
 | 
				
			||||||
    SystemConfigController,
 | 
					    SystemConfigController,
 | 
				
			||||||
    UserController,
 | 
					    UserController,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard],
 | 
					  providers: [{ provide: APP_GUARD, useExisting: AuthGuard }, AuthGuard],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class AppModule {}
 | 
					export class AppModule implements OnModuleInit {
 | 
				
			||||||
 | 
					  constructor(private searchService: SearchService) {}
 | 
				
			||||||
 | 
					  async onModuleInit() {
 | 
				
			||||||
 | 
					    await this.searchService.bootstrap();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ export * from './api-key.controller';
 | 
				
			|||||||
export * from './auth.controller';
 | 
					export * from './auth.controller';
 | 
				
			||||||
export * from './device-info.controller';
 | 
					export * from './device-info.controller';
 | 
				
			||||||
export * from './oauth.controller';
 | 
					export * from './oauth.controller';
 | 
				
			||||||
 | 
					export * from './search.controller';
 | 
				
			||||||
export * from './share.controller';
 | 
					export * from './share.controller';
 | 
				
			||||||
export * from './system-config.controller';
 | 
					export * from './system-config.controller';
 | 
				
			||||||
export * from './user.controller';
 | 
					export * from './user.controller';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								server/apps/immich/src/controllers/search.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								server/apps/immich/src/controllers/search.controller.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					import { AuthUserDto, SearchConfigResponseDto, SearchDto, SearchResponseDto, SearchService } from '@app/domain';
 | 
				
			||||||
 | 
					import { Controller, Get, Query, ValidationPipe } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ApiTags } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { GetAuthUser } from '../decorators/auth-user.decorator';
 | 
				
			||||||
 | 
					import { Authenticated } from '../decorators/authenticated.decorator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@ApiTags('Search')
 | 
				
			||||||
 | 
					@Authenticated()
 | 
				
			||||||
 | 
					@Controller('search')
 | 
				
			||||||
 | 
					export class SearchController {
 | 
				
			||||||
 | 
					  constructor(private readonly searchService: SearchService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Authenticated()
 | 
				
			||||||
 | 
					  @Get()
 | 
				
			||||||
 | 
					  async search(
 | 
				
			||||||
 | 
					    @GetAuthUser() authUser: AuthUserDto,
 | 
				
			||||||
 | 
					    @Query(new ValidationPipe({ transform: true })) dto: SearchDto,
 | 
				
			||||||
 | 
					  ): Promise<SearchResponseDto> {
 | 
				
			||||||
 | 
					    return this.searchService.search(authUser, dto);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Authenticated()
 | 
				
			||||||
 | 
					  @Get('config')
 | 
				
			||||||
 | 
					  getSearchConfig(): SearchConfigResponseDto {
 | 
				
			||||||
 | 
					    return this.searchService.getConfig();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -11,7 +11,7 @@ import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
 | 
				
			|||||||
import { json } from 'body-parser';
 | 
					import { json } from 'body-parser';
 | 
				
			||||||
import { patchOpenAPI } from './utils/patch-open-api.util';
 | 
					import { patchOpenAPI } from './utils/patch-open-api.util';
 | 
				
			||||||
import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
 | 
					import { getLogLevels, MACHINE_LEARNING_ENABLED } from '@app/common';
 | 
				
			||||||
import { IMMICH_ACCESS_COOKIE } from '@app/domain';
 | 
					import { IMMICH_ACCESS_COOKIE, SearchService } from '@app/domain';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const logger = new Logger('ImmichServer');
 | 
					const logger = new Logger('ImmichServer');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -73,6 +73,9 @@ async function bootstrap() {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const searchService = app.get(SearchService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  logger.warn(`Machine learning is ${MACHINE_LEARNING_ENABLED ? 'enabled' : 'disabled'}`);
 | 
					  logger.warn(`Machine learning is ${MACHINE_LEARNING_ENABLED ? 'enabled' : 'disabled'}`);
 | 
				
			||||||
 | 
					  logger.warn(`Search is ${searchService.isEnabled() ? 'enabled' : 'disabled'}`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
bootstrap();
 | 
					bootstrap();
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  BackgroundTaskProcessor,
 | 
					  BackgroundTaskProcessor,
 | 
				
			||||||
  MachineLearningProcessor,
 | 
					  MachineLearningProcessor,
 | 
				
			||||||
 | 
					  SearchIndexProcessor,
 | 
				
			||||||
  StorageTemplateMigrationProcessor,
 | 
					  StorageTemplateMigrationProcessor,
 | 
				
			||||||
  ThumbnailGeneratorProcessor,
 | 
					  ThumbnailGeneratorProcessor,
 | 
				
			||||||
} from './processors';
 | 
					} from './processors';
 | 
				
			||||||
@ -26,6 +27,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor'
 | 
				
			|||||||
    MachineLearningProcessor,
 | 
					    MachineLearningProcessor,
 | 
				
			||||||
    StorageTemplateMigrationProcessor,
 | 
					    StorageTemplateMigrationProcessor,
 | 
				
			||||||
    BackgroundTaskProcessor,
 | 
					    BackgroundTaskProcessor,
 | 
				
			||||||
 | 
					    SearchIndexProcessor,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class MicroservicesModule {}
 | 
					export class MicroservicesModule {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,15 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  AssetService,
 | 
					  AssetService,
 | 
				
			||||||
 | 
					  IAlbumJob,
 | 
				
			||||||
  IAssetJob,
 | 
					  IAssetJob,
 | 
				
			||||||
  IAssetUploadedJob,
 | 
					  IAssetUploadedJob,
 | 
				
			||||||
  IDeleteFilesJob,
 | 
					  IDeleteFilesJob,
 | 
				
			||||||
 | 
					  IDeleteJob,
 | 
				
			||||||
  IUserDeletionJob,
 | 
					  IUserDeletionJob,
 | 
				
			||||||
  JobName,
 | 
					  JobName,
 | 
				
			||||||
  MediaService,
 | 
					  MediaService,
 | 
				
			||||||
  QueueName,
 | 
					  QueueName,
 | 
				
			||||||
 | 
					  SearchService,
 | 
				
			||||||
  SmartInfoService,
 | 
					  SmartInfoService,
 | 
				
			||||||
  StorageService,
 | 
					  StorageService,
 | 
				
			||||||
  StorageTemplateService,
 | 
					  StorageTemplateService,
 | 
				
			||||||
@ -61,6 +64,41 @@ export class MachineLearningProcessor {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Processor(QueueName.SEARCH)
 | 
				
			||||||
 | 
					export class SearchIndexProcessor {
 | 
				
			||||||
 | 
					  constructor(private searchService: SearchService) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(JobName.SEARCH_INDEX_ALBUMS)
 | 
				
			||||||
 | 
					  async onIndexAlbums() {
 | 
				
			||||||
 | 
					    await this.searchService.handleIndexAlbums();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(JobName.SEARCH_INDEX_ASSETS)
 | 
				
			||||||
 | 
					  async onIndexAssets() {
 | 
				
			||||||
 | 
					    await this.searchService.handleIndexAssets();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(JobName.SEARCH_INDEX_ALBUM)
 | 
				
			||||||
 | 
					  async onIndexAlbum(job: Job<IAlbumJob>) {
 | 
				
			||||||
 | 
					    await this.searchService.handleIndexAlbum(job.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(JobName.SEARCH_INDEX_ASSET)
 | 
				
			||||||
 | 
					  async onIndexAsset(job: Job<IAssetJob>) {
 | 
				
			||||||
 | 
					    await this.searchService.handleIndexAsset(job.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(JobName.SEARCH_REMOVE_ALBUM)
 | 
				
			||||||
 | 
					  async onRemoveAlbum(job: Job<IDeleteJob>) {
 | 
				
			||||||
 | 
					    await this.searchService.handleRemoveAlbum(job.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Process(JobName.SEARCH_REMOVE_ASSET)
 | 
				
			||||||
 | 
					  async onRemoveAsset(job: Job<IDeleteJob>) {
 | 
				
			||||||
 | 
					    await this.searchService.handleRemoveAsset(job.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Processor(QueueName.STORAGE_TEMPLATE_MIGRATION)
 | 
					@Processor(QueueName.STORAGE_TEMPLATE_MIGRATION)
 | 
				
			||||||
export class StorageTemplateMigrationProcessor {
 | 
					export class StorageTemplateMigrationProcessor {
 | 
				
			||||||
  constructor(private storageTemplateService: StorageTemplateService) {}
 | 
					  constructor(private storageTemplateService: StorageTemplateService) {}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,26 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AssetCore,
 | 
				
			||||||
 | 
					  IAssetRepository,
 | 
				
			||||||
 | 
					  IAssetUploadedJob,
 | 
				
			||||||
 | 
					  IReverseGeocodingJob,
 | 
				
			||||||
 | 
					  ISearchRepository,
 | 
				
			||||||
 | 
					  JobName,
 | 
				
			||||||
 | 
					  QueueName,
 | 
				
			||||||
 | 
					} from '@app/domain';
 | 
				
			||||||
import { AssetEntity, AssetType, ExifEntity } from '@app/infra';
 | 
					import { AssetEntity, AssetType, ExifEntity } from '@app/infra';
 | 
				
			||||||
import { IReverseGeocodingJob, IAssetUploadedJob, QueueName, JobName, IAssetRepository } from '@app/domain';
 | 
					 | 
				
			||||||
import { Process, Processor } from '@nestjs/bull';
 | 
					import { Process, Processor } from '@nestjs/bull';
 | 
				
			||||||
import { Inject, Logger } from '@nestjs/common';
 | 
					import { Inject, Logger } from '@nestjs/common';
 | 
				
			||||||
import { ConfigService } from '@nestjs/config';
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Job } from 'bull';
 | 
					import { Job } from 'bull';
 | 
				
			||||||
 | 
					import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
 | 
				
			||||||
import ffmpeg from 'fluent-ffmpeg';
 | 
					import ffmpeg from 'fluent-ffmpeg';
 | 
				
			||||||
 | 
					import { getName } from 'i18n-iso-countries';
 | 
				
			||||||
 | 
					import geocoder, { InitOptions } from 'local-reverse-geocoder';
 | 
				
			||||||
 | 
					import fs from 'node:fs';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import sharp from 'sharp';
 | 
					import sharp from 'sharp';
 | 
				
			||||||
import { Repository } from 'typeorm/repository/Repository';
 | 
					import { Repository } from 'typeorm/repository/Repository';
 | 
				
			||||||
import geocoder, { InitOptions } from 'local-reverse-geocoder';
 | 
					 | 
				
			||||||
import { getName } from 'i18n-iso-countries';
 | 
					 | 
				
			||||||
import fs from 'node:fs';
 | 
					 | 
				
			||||||
import { ExifDateTime, exiftool, Tags } from 'exiftool-vendored';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ImmichTags extends Tags {
 | 
					interface ImmichTags extends Tags {
 | 
				
			||||||
  ContentIdentifier?: string;
 | 
					  ContentIdentifier?: string;
 | 
				
			||||||
@ -71,13 +79,19 @@ export type GeoData = {
 | 
				
			|||||||
export class MetadataExtractionProcessor {
 | 
					export class MetadataExtractionProcessor {
 | 
				
			||||||
  private logger = new Logger(MetadataExtractionProcessor.name);
 | 
					  private logger = new Logger(MetadataExtractionProcessor.name);
 | 
				
			||||||
  private isGeocodeInitialized = false;
 | 
					  private isGeocodeInitialized = false;
 | 
				
			||||||
 | 
					  private assetCore: AssetCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
					    @Inject(IAssetRepository) assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(ISearchRepository) searchRepository: ISearchRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @InjectRepository(ExifEntity)
 | 
					    @InjectRepository(ExifEntity)
 | 
				
			||||||
    private exifRepository: Repository<ExifEntity>,
 | 
					    private exifRepository: Repository<ExifEntity>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    configService: ConfigService,
 | 
					    configService: ConfigService,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.assetCore = new AssetCore(assetRepository, searchRepository);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!configService.get('DISABLE_REVERSE_GEOCODING')) {
 | 
					    if (!configService.get('DISABLE_REVERSE_GEOCODING')) {
 | 
				
			||||||
      this.logger.log('Initializing Reverse Geocoding');
 | 
					      this.logger.log('Initializing Reverse Geocoding');
 | 
				
			||||||
      geocoderInit({
 | 
					      geocoderInit({
 | 
				
			||||||
@ -175,20 +189,11 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      newExif.longitude = exifData?.GPSLongitude || null;
 | 
					      newExif.longitude = exifData?.GPSLongitude || null;
 | 
				
			||||||
      newExif.livePhotoCID = exifData?.MediaGroupUUID || null;
 | 
					      newExif.livePhotoCID = exifData?.MediaGroupUUID || null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.assetRepository.save({
 | 
					 | 
				
			||||||
        id: asset.id,
 | 
					 | 
				
			||||||
        fileCreatedAt: fileCreatedAt?.toISOString(),
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
 | 
					      if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
 | 
				
			||||||
        const motionAsset = await this.assetRepository.findLivePhotoMatch(
 | 
					        const motionAsset = await this.assetCore.findLivePhotoMatch(newExif.livePhotoCID, asset.id, AssetType.VIDEO);
 | 
				
			||||||
          newExif.livePhotoCID,
 | 
					 | 
				
			||||||
          asset.id,
 | 
					 | 
				
			||||||
          AssetType.VIDEO,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if (motionAsset) {
 | 
					        if (motionAsset) {
 | 
				
			||||||
          await this.assetRepository.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
 | 
					          await this.assetCore.save({ id: asset.id, livePhotoVideoId: motionAsset.id });
 | 
				
			||||||
          await this.assetRepository.save({ id: motionAsset.id, isVisible: false });
 | 
					          await this.assetCore.save({ id: motionAsset.id, isVisible: false });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -226,6 +231,7 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
 | 
					      await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
 | 
				
			||||||
 | 
					      await this.assetCore.save({ id: asset.id, fileCreatedAt: fileCreatedAt?.toISOString() });
 | 
				
			||||||
    } catch (error: any) {
 | 
					    } catch (error: any) {
 | 
				
			||||||
      this.logger.error(`Error extracting EXIF ${error}`, error?.stack);
 | 
					      this.logger.error(`Error extracting EXIF ${error}`, error?.stack);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -292,14 +298,10 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      newExif.livePhotoCID = exifData?.ContentIdentifier || null;
 | 
					      newExif.livePhotoCID = exifData?.ContentIdentifier || null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (newExif.livePhotoCID) {
 | 
					      if (newExif.livePhotoCID) {
 | 
				
			||||||
        const photoAsset = await this.assetRepository.findLivePhotoMatch(
 | 
					        const photoAsset = await this.assetCore.findLivePhotoMatch(newExif.livePhotoCID, asset.id, AssetType.IMAGE);
 | 
				
			||||||
          newExif.livePhotoCID,
 | 
					 | 
				
			||||||
          asset.id,
 | 
					 | 
				
			||||||
          AssetType.IMAGE,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        if (photoAsset) {
 | 
					        if (photoAsset) {
 | 
				
			||||||
          await this.assetRepository.save({ id: photoAsset.id, livePhotoVideoId: asset.id });
 | 
					          await this.assetCore.save({ id: photoAsset.id, livePhotoVideoId: asset.id });
 | 
				
			||||||
          await this.assetRepository.save({ id: asset.id, isVisible: false });
 | 
					          await this.assetCore.save({ id: asset.id, isVisible: false });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -355,7 +357,7 @@ export class MetadataExtractionProcessor {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
 | 
					      await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] });
 | 
				
			||||||
      await this.assetRepository.save({ id: asset.id, duration: durationString, fileCreatedAt });
 | 
					      await this.assetCore.save({ id: asset.id, duration: durationString, fileCreatedAt });
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      ``;
 | 
					      ``;
 | 
				
			||||||
      // do nothing
 | 
					      // do nothing
 | 
				
			||||||
 | 
				
			|||||||
@ -544,6 +544,171 @@
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "/search": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "operationId": "search",
 | 
				
			||||||
 | 
					        "description": "",
 | 
				
			||||||
 | 
					        "parameters": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "query",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "type",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "enum": [
 | 
				
			||||||
 | 
					                "IMAGE",
 | 
				
			||||||
 | 
					                "VIDEO",
 | 
				
			||||||
 | 
					                "AUDIO",
 | 
				
			||||||
 | 
					                "OTHER"
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "isFavorite",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "boolean"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "exifInfo.city",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "exifInfo.state",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "exifInfo.country",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "exifInfo.make",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "exifInfo.model",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "string"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "smartInfo.objects",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "array",
 | 
				
			||||||
 | 
					              "items": {
 | 
				
			||||||
 | 
					                "type": "string"
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "name": "smartInfo.tags",
 | 
				
			||||||
 | 
					            "required": false,
 | 
				
			||||||
 | 
					            "in": "query",
 | 
				
			||||||
 | 
					            "schema": {
 | 
				
			||||||
 | 
					              "type": "array",
 | 
				
			||||||
 | 
					              "items": {
 | 
				
			||||||
 | 
					                "type": "string"
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "description": "",
 | 
				
			||||||
 | 
					            "content": {
 | 
				
			||||||
 | 
					              "application/json": {
 | 
				
			||||||
 | 
					                "schema": {
 | 
				
			||||||
 | 
					                  "$ref": "#/components/schemas/SearchResponseDto"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "Search"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "security": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "bearer": []
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "cookie": []
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "bearer": []
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "cookie": []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "/search/config": {
 | 
				
			||||||
 | 
					      "get": {
 | 
				
			||||||
 | 
					        "operationId": "getSearchConfig",
 | 
				
			||||||
 | 
					        "description": "",
 | 
				
			||||||
 | 
					        "parameters": [],
 | 
				
			||||||
 | 
					        "responses": {
 | 
				
			||||||
 | 
					          "200": {
 | 
				
			||||||
 | 
					            "description": "",
 | 
				
			||||||
 | 
					            "content": {
 | 
				
			||||||
 | 
					              "application/json": {
 | 
				
			||||||
 | 
					                "schema": {
 | 
				
			||||||
 | 
					                  "$ref": "#/components/schemas/SearchConfigResponseDto"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "Search"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        "security": [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "bearer": []
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "cookie": []
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "bearer": []
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            "cookie": []
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "/share": {
 | 
					    "/share": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "operationId": "getAllSharedLinks",
 | 
					        "operationId": "getAllSharedLinks",
 | 
				
			||||||
@ -3554,13 +3719,6 @@
 | 
				
			|||||||
          "url"
 | 
					          "url"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "SharedLinkType": {
 | 
					 | 
				
			||||||
        "type": "string",
 | 
					 | 
				
			||||||
        "enum": [
 | 
					 | 
				
			||||||
          "ALBUM",
 | 
					 | 
				
			||||||
          "INDIVIDUAL"
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "AssetTypeEnum": {
 | 
					      "AssetTypeEnum": {
 | 
				
			||||||
        "type": "string",
 | 
					        "type": "string",
 | 
				
			||||||
        "enum": [
 | 
					        "enum": [
 | 
				
			||||||
@ -3871,6 +4029,130 @@
 | 
				
			|||||||
          "owner"
 | 
					          "owner"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchFacetCountResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "count": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "value": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "count",
 | 
				
			||||||
 | 
					          "value"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchFacetResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "fieldName": {
 | 
				
			||||||
 | 
					            "type": "string"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "counts": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/SearchFacetCountResponseDto"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "fieldName",
 | 
				
			||||||
 | 
					          "counts"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchAlbumResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "total": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "count": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/AlbumResponseDto"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "facets": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/SearchFacetResponseDto"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "total",
 | 
				
			||||||
 | 
					          "count",
 | 
				
			||||||
 | 
					          "items",
 | 
				
			||||||
 | 
					          "facets"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchAssetResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "total": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "count": {
 | 
				
			||||||
 | 
					            "type": "integer"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "items": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/AssetResponseDto"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "facets": {
 | 
				
			||||||
 | 
					            "type": "array",
 | 
				
			||||||
 | 
					            "items": {
 | 
				
			||||||
 | 
					              "$ref": "#/components/schemas/SearchFacetResponseDto"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "total",
 | 
				
			||||||
 | 
					          "count",
 | 
				
			||||||
 | 
					          "items",
 | 
				
			||||||
 | 
					          "facets"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "albums": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/SearchAlbumResponseDto"
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          "assets": {
 | 
				
			||||||
 | 
					            "$ref": "#/components/schemas/SearchAssetResponseDto"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "albums",
 | 
				
			||||||
 | 
					          "assets"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SearchConfigResponseDto": {
 | 
				
			||||||
 | 
					        "type": "object",
 | 
				
			||||||
 | 
					        "properties": {
 | 
				
			||||||
 | 
					          "enabled": {
 | 
				
			||||||
 | 
					            "type": "boolean"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "required": [
 | 
				
			||||||
 | 
					          "enabled"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "SharedLinkType": {
 | 
				
			||||||
 | 
					        "type": "string",
 | 
				
			||||||
 | 
					        "enum": [
 | 
				
			||||||
 | 
					          "ALBUM",
 | 
				
			||||||
 | 
					          "INDIVIDUAL"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "SharedLinkResponseDto": {
 | 
					      "SharedLinkResponseDto": {
 | 
				
			||||||
        "type": "object",
 | 
					        "type": "object",
 | 
				
			||||||
        "properties": {
 | 
					        "properties": {
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,11 @@ export const immichAppConfig: ConfigModuleOptions = {
 | 
				
			|||||||
    DB_PASSWORD: WHEN_DB_URL_SET,
 | 
					    DB_PASSWORD: WHEN_DB_URL_SET,
 | 
				
			||||||
    DB_DATABASE_NAME: WHEN_DB_URL_SET,
 | 
					    DB_DATABASE_NAME: WHEN_DB_URL_SET,
 | 
				
			||||||
    DB_URL: Joi.string().optional(),
 | 
					    DB_URL: Joi.string().optional(),
 | 
				
			||||||
 | 
					    TYPESENSE_API_KEY: Joi.when('TYPESENSE_ENABLED', {
 | 
				
			||||||
 | 
					      is: 'false',
 | 
				
			||||||
 | 
					      then: Joi.string().optional(),
 | 
				
			||||||
 | 
					      otherwise: Joi.string().required(),
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
    DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
 | 
					    DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false),
 | 
				
			||||||
    REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3),
 | 
					    REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0, 1, 2, 3).default(3),
 | 
				
			||||||
    LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose', 'debug', 'log', 'warn', 'error').default('log'),
 | 
					    LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose', 'debug', 'log', 'warn', 'error').default('log'),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,9 @@
 | 
				
			|||||||
 | 
					import { AlbumEntity } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const IAlbumRepository = 'IAlbumRepository';
 | 
					export const IAlbumRepository = 'IAlbumRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAlbumRepository {
 | 
					export interface IAlbumRepository {
 | 
				
			||||||
  deleteAll(userId: string): Promise<void>;
 | 
					  deleteAll(userId: string): Promise<void>;
 | 
				
			||||||
 | 
					  getAll(): Promise<AlbumEntity[]>;
 | 
				
			||||||
 | 
					  save(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								server/libs/domain/src/asset/asset.core.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								server/libs/domain/src/asset/asset.core.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					import { ISearchRepository, SearchCollection } from '../search/search.repository';
 | 
				
			||||||
 | 
					import { AssetSearchOptions, IAssetRepository } from './asset.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class AssetCore {
 | 
				
			||||||
 | 
					  constructor(private repository: IAssetRepository, private searchRepository: ISearchRepository) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAll(options: AssetSearchOptions) {
 | 
				
			||||||
 | 
					    return this.repository.getAll(options);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async save(asset: Partial<AssetEntity>) {
 | 
				
			||||||
 | 
					    const _asset = await this.repository.save(asset);
 | 
				
			||||||
 | 
					    await this.searchRepository.index(SearchCollection.ASSETS, _asset);
 | 
				
			||||||
 | 
					    return _asset;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null> {
 | 
				
			||||||
 | 
					    return this.repository.findLivePhotoMatch(livePhotoCID, otherAssetId, type);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,10 +1,14 @@
 | 
				
			|||||||
import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface AssetSearchOptions {
 | 
				
			||||||
 | 
					  isVisible?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const IAssetRepository = 'IAssetRepository';
 | 
					export const IAssetRepository = 'IAssetRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAssetRepository {
 | 
					export interface IAssetRepository {
 | 
				
			||||||
  deleteAll(ownerId: string): Promise<void>;
 | 
					  deleteAll(ownerId: string): Promise<void>;
 | 
				
			||||||
  getAll(): Promise<AssetEntity[]>;
 | 
					  getAll(options?: AssetSearchOptions): Promise<AssetEntity[]>;
 | 
				
			||||||
  save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
 | 
					  save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
 | 
				
			||||||
  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null>;
 | 
					  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,25 @@
 | 
				
			|||||||
import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
import { newJobRepositoryMock } from '../../test';
 | 
					import { assetEntityStub, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
 | 
				
			||||||
import { AssetService } from '../asset';
 | 
					import { newSearchRepositoryMock } from '../../test/search.repository.mock';
 | 
				
			||||||
 | 
					import { AssetService, IAssetRepository } from '../asset';
 | 
				
			||||||
import { IJobRepository, JobName } from '../job';
 | 
					import { IJobRepository, JobName } from '../job';
 | 
				
			||||||
 | 
					import { ISearchRepository } from '../search';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(AssetService.name, () => {
 | 
					describe(AssetService.name, () => {
 | 
				
			||||||
  let sut: AssetService;
 | 
					  let sut: AssetService;
 | 
				
			||||||
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
  let jobMock: jest.Mocked<IJobRepository>;
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
 | 
					  let searchMock: jest.Mocked<ISearchRepository>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should work', () => {
 | 
					  it('should work', () => {
 | 
				
			||||||
    expect(sut).toBeDefined();
 | 
					    expect(sut).toBeDefined();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
    sut = new AssetService(jobMock);
 | 
					    searchMock = newSearchRepositoryMock();
 | 
				
			||||||
 | 
					    sut = new AssetService(assetMock, jobMock, searchMock);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe(`handle asset upload`, () => {
 | 
					  describe(`handle asset upload`, () => {
 | 
				
			||||||
@ -42,4 +48,15 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('save', () => {
 | 
				
			||||||
 | 
					    it('should save an asset', async () => {
 | 
				
			||||||
 | 
					      assetMock.save.mockResolvedValue(assetEntityStub.image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.save(assetEntityStub.image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith(assetEntityStub.image);
 | 
				
			||||||
 | 
					      expect(searchMock.index).toHaveBeenCalledWith('assets', assetEntityStub.image);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,20 @@
 | 
				
			|||||||
import { AssetType } from '@app/infra/db/entities';
 | 
					import { AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
import { Inject } from '@nestjs/common';
 | 
					import { Inject } from '@nestjs/common';
 | 
				
			||||||
import { IAssetUploadedJob, IJobRepository, JobName } from '../job';
 | 
					import { IAssetUploadedJob, IJobRepository, JobName } from '../job';
 | 
				
			||||||
 | 
					import { ISearchRepository } from '../search';
 | 
				
			||||||
 | 
					import { AssetCore } from './asset.core';
 | 
				
			||||||
 | 
					import { IAssetRepository } from './asset.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class AssetService {
 | 
					export class AssetService {
 | 
				
			||||||
  constructor(@Inject(IJobRepository) private jobRepository: IJobRepository) {}
 | 
					  private assetCore: AssetCore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @Inject(IAssetRepository) assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
 | 
					    @Inject(ISearchRepository) searchRepository: ISearchRepository,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.assetCore = new AssetCore(assetRepository, searchRepository);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleAssetUpload(data: IAssetUploadedJob) {
 | 
					  async handleAssetUpload(data: IAssetUploadedJob) {
 | 
				
			||||||
    await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data });
 | 
					    await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data });
 | 
				
			||||||
@ -15,4 +26,8 @@ export class AssetService {
 | 
				
			|||||||
      await this.jobRepository.queue({ name: JobName.EXIF_EXTRACTION, data });
 | 
					      await this.jobRepository.queue({ name: JobName.EXIF_EXTRACTION, data });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  save(asset: Partial<AssetEntity>) {
 | 
				
			||||||
 | 
					    return this.assetCore.save(asset);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					export * from './asset.core';
 | 
				
			||||||
export * from './asset.repository';
 | 
					export * from './asset.repository';
 | 
				
			||||||
export * from './asset.service';
 | 
					export * from './asset.service';
 | 
				
			||||||
export * from './response-dto';
 | 
					export * from './response-dto';
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { AuthService } from './auth';
 | 
				
			|||||||
import { DeviceInfoService } from './device-info';
 | 
					import { DeviceInfoService } from './device-info';
 | 
				
			||||||
import { MediaService } from './media';
 | 
					import { MediaService } from './media';
 | 
				
			||||||
import { OAuthService } from './oauth';
 | 
					import { OAuthService } from './oauth';
 | 
				
			||||||
 | 
					import { SearchService } from './search';
 | 
				
			||||||
import { ShareService } from './share';
 | 
					import { ShareService } from './share';
 | 
				
			||||||
import { SmartInfoService } from './smart-info';
 | 
					import { SmartInfoService } from './smart-info';
 | 
				
			||||||
import { StorageService } from './storage';
 | 
					import { StorageService } from './storage';
 | 
				
			||||||
@ -25,6 +26,7 @@ const providers: Provider[] = [
 | 
				
			|||||||
  SystemConfigService,
 | 
					  SystemConfigService,
 | 
				
			||||||
  UserService,
 | 
					  UserService,
 | 
				
			||||||
  ShareService,
 | 
					  ShareService,
 | 
				
			||||||
 | 
					  SearchService,
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    provide: INITIAL_SYSTEM_CONFIG,
 | 
					    provide: INITIAL_SYSTEM_CONFIG,
 | 
				
			||||||
    inject: [SystemConfigService],
 | 
					    inject: [SystemConfigService],
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ export * from './domain.module';
 | 
				
			|||||||
export * from './job';
 | 
					export * from './job';
 | 
				
			||||||
export * from './media';
 | 
					export * from './media';
 | 
				
			||||||
export * from './oauth';
 | 
					export * from './oauth';
 | 
				
			||||||
 | 
					export * from './search';
 | 
				
			||||||
export * from './share';
 | 
					export * from './share';
 | 
				
			||||||
export * from './smart-info';
 | 
					export * from './smart-info';
 | 
				
			||||||
export * from './storage';
 | 
					export * from './storage';
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ export enum QueueName {
 | 
				
			|||||||
  MACHINE_LEARNING = 'machine-learning-queue',
 | 
					  MACHINE_LEARNING = 'machine-learning-queue',
 | 
				
			||||||
  BACKGROUND_TASK = 'background-task',
 | 
					  BACKGROUND_TASK = 'background-task',
 | 
				
			||||||
  STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration-queue',
 | 
					  STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration-queue',
 | 
				
			||||||
 | 
					  SEARCH = 'search-queue',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum JobName {
 | 
					export enum JobName {
 | 
				
			||||||
@ -22,4 +23,10 @@ export enum JobName {
 | 
				
			|||||||
  OBJECT_DETECTION = 'detect-object',
 | 
					  OBJECT_DETECTION = 'detect-object',
 | 
				
			||||||
  IMAGE_TAGGING = 'tag-image',
 | 
					  IMAGE_TAGGING = 'tag-image',
 | 
				
			||||||
  DELETE_FILES = 'delete-files',
 | 
					  DELETE_FILES = 'delete-files',
 | 
				
			||||||
 | 
					  SEARCH_INDEX_ASSETS = 'search-index-assets',
 | 
				
			||||||
 | 
					  SEARCH_INDEX_ASSET = 'search-index-asset',
 | 
				
			||||||
 | 
					  SEARCH_INDEX_ALBUMS = 'search-index-albums',
 | 
				
			||||||
 | 
					  SEARCH_INDEX_ALBUM = 'search-index-album',
 | 
				
			||||||
 | 
					  SEARCH_REMOVE_ALBUM = 'search-remove-album',
 | 
				
			||||||
 | 
					  SEARCH_REMOVE_ASSET = 'search-remove-asset',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,8 @@
 | 
				
			|||||||
import { AssetEntity, UserEntity } from '@app/infra/db/entities';
 | 
					import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IAlbumJob {
 | 
				
			||||||
 | 
					  album: AlbumEntity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IAssetJob {
 | 
					export interface IAssetJob {
 | 
				
			||||||
  asset: AssetEntity;
 | 
					  asset: AssetEntity;
 | 
				
			||||||
@ -9,6 +13,10 @@ export interface IAssetUploadedJob {
 | 
				
			|||||||
  fileName: string;
 | 
					  fileName: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IDeleteJob {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IDeleteFilesJob {
 | 
					export interface IDeleteFilesJob {
 | 
				
			||||||
  files: Array<string | null | undefined>;
 | 
					  files: Array<string | null | undefined>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,13 @@
 | 
				
			|||||||
import { JobName, QueueName } from './job.constants';
 | 
					import { JobName, QueueName } from './job.constants';
 | 
				
			||||||
import { IAssetJob, IAssetUploadedJob, IDeleteFilesJob, IReverseGeocodingJob, IUserDeletionJob } from './job.interface';
 | 
					import {
 | 
				
			||||||
 | 
					  IAlbumJob,
 | 
				
			||||||
 | 
					  IAssetJob,
 | 
				
			||||||
 | 
					  IAssetUploadedJob,
 | 
				
			||||||
 | 
					  IDeleteFilesJob,
 | 
				
			||||||
 | 
					  IDeleteJob,
 | 
				
			||||||
 | 
					  IReverseGeocodingJob,
 | 
				
			||||||
 | 
					  IUserDeletionJob,
 | 
				
			||||||
 | 
					} from './job.interface';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface JobCounts {
 | 
					export interface JobCounts {
 | 
				
			||||||
  active: number;
 | 
					  active: number;
 | 
				
			||||||
@ -23,7 +31,13 @@ export type JobItem =
 | 
				
			|||||||
  | { name: JobName.EXTRACT_VIDEO_METADATA; data: IAssetUploadedJob }
 | 
					  | { name: JobName.EXTRACT_VIDEO_METADATA; data: IAssetUploadedJob }
 | 
				
			||||||
  | { name: JobName.OBJECT_DETECTION; data: IAssetJob }
 | 
					  | { name: JobName.OBJECT_DETECTION; data: IAssetJob }
 | 
				
			||||||
  | { name: JobName.IMAGE_TAGGING; data: IAssetJob }
 | 
					  | { name: JobName.IMAGE_TAGGING; data: IAssetJob }
 | 
				
			||||||
  | { name: JobName.DELETE_FILES; data: IDeleteFilesJob };
 | 
					  | { name: JobName.DELETE_FILES; data: IDeleteFilesJob }
 | 
				
			||||||
 | 
					  | { name: JobName.SEARCH_INDEX_ASSETS }
 | 
				
			||||||
 | 
					  | { name: JobName.SEARCH_INDEX_ASSET; data: IAssetJob }
 | 
				
			||||||
 | 
					  | { name: JobName.SEARCH_INDEX_ALBUMS }
 | 
				
			||||||
 | 
					  | { name: JobName.SEARCH_INDEX_ALBUM; data: IAlbumJob }
 | 
				
			||||||
 | 
					  | { name: JobName.SEARCH_REMOVE_ASSET; data: IDeleteJob }
 | 
				
			||||||
 | 
					  | { name: JobName.SEARCH_REMOVE_ALBUM; data: IDeleteJob };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const IJobRepository = 'IJobRepository';
 | 
					export const IJobRepository = 'IJobRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								server/libs/domain/src/search/dto/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/libs/domain/src/search/dto/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './search.dto';
 | 
				
			||||||
							
								
								
									
										57
									
								
								server/libs/domain/src/search/dto/search.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								server/libs/domain/src/search/dto/search.dto.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					import { AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					import { Transform } from 'class-transformer';
 | 
				
			||||||
 | 
					import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
 | 
				
			||||||
 | 
					import { toBoolean } from '../../../../../apps/immich/src/utils/transform.util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SearchDto {
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  query?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsEnum(AssetType)
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  type?: AssetType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsBoolean()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  @Transform(toBoolean)
 | 
				
			||||||
 | 
					  isFavorite?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  'exifInfo.city'?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  'exifInfo.state'?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  'exifInfo.country'?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  'exifInfo.make'?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString()
 | 
				
			||||||
 | 
					  @IsNotEmpty()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  'exifInfo.model'?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString({ each: true })
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  @Transform(({ value }) => value.split(','))
 | 
				
			||||||
 | 
					  'smartInfo.objects'?: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @IsString({ each: true })
 | 
				
			||||||
 | 
					  @IsArray()
 | 
				
			||||||
 | 
					  @IsOptional()
 | 
				
			||||||
 | 
					  @Transform(({ value }) => value.split(','))
 | 
				
			||||||
 | 
					  'smartInfo.tags'?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								server/libs/domain/src/search/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/libs/domain/src/search/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					export * from './dto';
 | 
				
			||||||
 | 
					export * from './response-dto';
 | 
				
			||||||
 | 
					export * from './search.repository';
 | 
				
			||||||
 | 
					export * from './search.service';
 | 
				
			||||||
							
								
								
									
										2
									
								
								server/libs/domain/src/search/response-dto/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								server/libs/domain/src/search/response-dto/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export * from './search-config-response.dto';
 | 
				
			||||||
 | 
					export * from './search-response.dto';
 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					export class SearchConfigResponseDto {
 | 
				
			||||||
 | 
					  enabled!: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { ApiProperty } from '@nestjs/swagger';
 | 
				
			||||||
 | 
					import { AlbumResponseDto } from '../../album';
 | 
				
			||||||
 | 
					import { AssetResponseDto } from '../../asset';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchFacetCountResponseDto {
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  count!: number;
 | 
				
			||||||
 | 
					  value!: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchFacetResponseDto {
 | 
				
			||||||
 | 
					  fieldName!: string;
 | 
				
			||||||
 | 
					  counts!: SearchFacetCountResponseDto[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchAlbumResponseDto {
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  total!: number;
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  count!: number;
 | 
				
			||||||
 | 
					  items!: AlbumResponseDto[];
 | 
				
			||||||
 | 
					  facets!: SearchFacetResponseDto[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchAssetResponseDto {
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  total!: number;
 | 
				
			||||||
 | 
					  @ApiProperty({ type: 'integer' })
 | 
				
			||||||
 | 
					  count!: number;
 | 
				
			||||||
 | 
					  items!: AssetResponseDto[];
 | 
				
			||||||
 | 
					  facets!: SearchFacetResponseDto[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class SearchResponseDto {
 | 
				
			||||||
 | 
					  albums!: SearchAlbumResponseDto;
 | 
				
			||||||
 | 
					  assets!: SearchAssetResponseDto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										60
									
								
								server/libs/domain/src/search/search.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								server/libs/domain/src/search/search.repository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					import { AlbumEntity, AssetEntity, AssetType } from '@app/infra/db/entities';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export enum SearchCollection {
 | 
				
			||||||
 | 
					  ASSETS = 'assets',
 | 
				
			||||||
 | 
					  ALBUMS = 'albums',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SearchFilter {
 | 
				
			||||||
 | 
					  id?: string;
 | 
				
			||||||
 | 
					  userId: string;
 | 
				
			||||||
 | 
					  type?: AssetType;
 | 
				
			||||||
 | 
					  isFavorite?: boolean;
 | 
				
			||||||
 | 
					  city?: string;
 | 
				
			||||||
 | 
					  state?: string;
 | 
				
			||||||
 | 
					  country?: string;
 | 
				
			||||||
 | 
					  make?: string;
 | 
				
			||||||
 | 
					  model?: string;
 | 
				
			||||||
 | 
					  objects?: string[];
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SearchResult<T> {
 | 
				
			||||||
 | 
					  /** total matches */
 | 
				
			||||||
 | 
					  total: number;
 | 
				
			||||||
 | 
					  /** collection size */
 | 
				
			||||||
 | 
					  count: number;
 | 
				
			||||||
 | 
					  /** current page */
 | 
				
			||||||
 | 
					  page: number;
 | 
				
			||||||
 | 
					  /** items for page */
 | 
				
			||||||
 | 
					  items: T[];
 | 
				
			||||||
 | 
					  facets: SearchFacet[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SearchFacet {
 | 
				
			||||||
 | 
					  fieldName: string;
 | 
				
			||||||
 | 
					  counts: Array<{
 | 
				
			||||||
 | 
					    count: number;
 | 
				
			||||||
 | 
					    value: string;
 | 
				
			||||||
 | 
					  }>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SearchCollectionIndexStatus = Record<SearchCollection, boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ISearchRepository = 'ISearchRepository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ISearchRepository {
 | 
				
			||||||
 | 
					  setup(): Promise<void>;
 | 
				
			||||||
 | 
					  checkMigrationStatus(): Promise<SearchCollectionIndexStatus>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  index(collection: SearchCollection.ASSETS, item: AssetEntity): Promise<void>;
 | 
				
			||||||
 | 
					  index(collection: SearchCollection.ALBUMS, item: AlbumEntity): Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  delete(collection: SearchCollection, id: string): Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  import(collection: SearchCollection.ASSETS, items: AssetEntity[], done: boolean): Promise<void>;
 | 
				
			||||||
 | 
					  import(collection: SearchCollection.ALBUMS, items: AlbumEntity[], done: boolean): Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  search(collection: SearchCollection.ASSETS, query: string, filters: SearchFilter): Promise<SearchResult<AssetEntity>>;
 | 
				
			||||||
 | 
					  search(collection: SearchCollection.ALBUMS, query: string, filters: SearchFilter): Promise<SearchResult<AlbumEntity>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										317
									
								
								server/libs/domain/src/search/search.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										317
									
								
								server/libs/domain/src/search/search.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,317 @@
 | 
				
			|||||||
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { plainToInstance } from 'class-transformer';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  albumStub,
 | 
				
			||||||
 | 
					  assetEntityStub,
 | 
				
			||||||
 | 
					  authStub,
 | 
				
			||||||
 | 
					  newAlbumRepositoryMock,
 | 
				
			||||||
 | 
					  newAssetRepositoryMock,
 | 
				
			||||||
 | 
					  newJobRepositoryMock,
 | 
				
			||||||
 | 
					  newSearchRepositoryMock,
 | 
				
			||||||
 | 
					} from '../../test';
 | 
				
			||||||
 | 
					import { IAlbumRepository } from '../album/album.repository';
 | 
				
			||||||
 | 
					import { IAssetRepository } from '../asset/asset.repository';
 | 
				
			||||||
 | 
					import { JobName } from '../job';
 | 
				
			||||||
 | 
					import { IJobRepository } from '../job/job.repository';
 | 
				
			||||||
 | 
					import { SearchDto } from './dto';
 | 
				
			||||||
 | 
					import { ISearchRepository } from './search.repository';
 | 
				
			||||||
 | 
					import { SearchService } from './search.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe(SearchService.name, () => {
 | 
				
			||||||
 | 
					  let sut: SearchService;
 | 
				
			||||||
 | 
					  let albumMock: jest.Mocked<IAlbumRepository>;
 | 
				
			||||||
 | 
					  let assetMock: jest.Mocked<IAssetRepository>;
 | 
				
			||||||
 | 
					  let jobMock: jest.Mocked<IJobRepository>;
 | 
				
			||||||
 | 
					  let searchMock: jest.Mocked<ISearchRepository>;
 | 
				
			||||||
 | 
					  let configMock: jest.Mocked<ConfigService>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  beforeEach(() => {
 | 
				
			||||||
 | 
					    albumMock = newAlbumRepositoryMock();
 | 
				
			||||||
 | 
					    assetMock = newAssetRepositoryMock();
 | 
				
			||||||
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
 | 
					    searchMock = newSearchRepositoryMock();
 | 
				
			||||||
 | 
					    configMock = { get: jest.fn() } as unknown as jest.Mocked<ConfigService>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should work', () => {
 | 
				
			||||||
 | 
					    expect(sut).toBeDefined();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('request dto', () => {
 | 
				
			||||||
 | 
					    it('should convert smartInfo.tags to a string list', () => {
 | 
				
			||||||
 | 
					      const instance = plainToInstance(SearchDto, { 'smartInfo.tags': 'a,b,c' });
 | 
				
			||||||
 | 
					      expect(instance['smartInfo.tags']).toEqual(['a', 'b', 'c']);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle empty smartInfo.tags', () => {
 | 
				
			||||||
 | 
					      const instance = plainToInstance(SearchDto, {});
 | 
				
			||||||
 | 
					      expect(instance['smartInfo.tags']).toBeUndefined();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should convert smartInfo.objects to a string list', () => {
 | 
				
			||||||
 | 
					      const instance = plainToInstance(SearchDto, { 'smartInfo.objects': 'a,b,c' });
 | 
				
			||||||
 | 
					      expect(instance['smartInfo.objects']).toEqual(['a', 'b', 'c']);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle empty smartInfo.objects', () => {
 | 
				
			||||||
 | 
					      const instance = plainToInstance(SearchDto, {});
 | 
				
			||||||
 | 
					      expect(instance['smartInfo.objects']).toBeUndefined();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('isEnabled', () => {
 | 
				
			||||||
 | 
					    it('should be enabled by default', () => {
 | 
				
			||||||
 | 
					      expect(sut.isEnabled()).toBe(true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should be disabled via an env variable', () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(sut.isEnabled()).toBe(false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('getConfig', () => {
 | 
				
			||||||
 | 
					    it('should return the config', () => {
 | 
				
			||||||
 | 
					      expect(sut.getConfig()).toEqual({ enabled: true });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return the config when search is disabled', () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(sut.getConfig()).toEqual({ enabled: false });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe(`bootstrap`, () => {
 | 
				
			||||||
 | 
					    it('should skip when search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.bootstrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.setup).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(searchMock.checkMigrationStatus).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should skip schema migration if not needed', async () => {
 | 
				
			||||||
 | 
					      searchMock.checkMigrationStatus.mockResolvedValue({ assets: false, albums: false });
 | 
				
			||||||
 | 
					      await sut.bootstrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.setup).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should do schema migration if needed', async () => {
 | 
				
			||||||
 | 
					      searchMock.checkMigrationStatus.mockResolvedValue({ assets: true, albums: true });
 | 
				
			||||||
 | 
					      await sut.bootstrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.setup).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        [{ name: JobName.SEARCH_INDEX_ASSETS }],
 | 
				
			||||||
 | 
					        [{ name: JobName.SEARCH_INDEX_ALBUMS }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('search', () => {
 | 
				
			||||||
 | 
					    it('should throw an error is search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.search(authStub.admin, {})).rejects.toBeInstanceOf(BadRequestException);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.search).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should search assets and albums', async () => {
 | 
				
			||||||
 | 
					      searchMock.search.mockResolvedValue({
 | 
				
			||||||
 | 
					        total: 0,
 | 
				
			||||||
 | 
					        count: 0,
 | 
				
			||||||
 | 
					        page: 1,
 | 
				
			||||||
 | 
					        items: [],
 | 
				
			||||||
 | 
					        facets: [],
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.search(authStub.admin, {})).resolves.toEqual({
 | 
				
			||||||
 | 
					        albums: {
 | 
				
			||||||
 | 
					          total: 0,
 | 
				
			||||||
 | 
					          count: 0,
 | 
				
			||||||
 | 
					          page: 1,
 | 
				
			||||||
 | 
					          items: [],
 | 
				
			||||||
 | 
					          facets: [],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        assets: {
 | 
				
			||||||
 | 
					          total: 0,
 | 
				
			||||||
 | 
					          count: 0,
 | 
				
			||||||
 | 
					          page: 1,
 | 
				
			||||||
 | 
					          items: [],
 | 
				
			||||||
 | 
					          facets: [],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.search.mock.calls).toEqual([
 | 
				
			||||||
 | 
					        ['assets', '*', { userId: authStub.admin.id }],
 | 
				
			||||||
 | 
					        ['albums', '*', { userId: authStub.admin.id }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleIndexAssets', () => {
 | 
				
			||||||
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAssets();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.import).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should index all the assets', async () => {
 | 
				
			||||||
 | 
					      assetMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAssets();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.import).toHaveBeenCalledWith('assets', [], true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should log an error', async () => {
 | 
				
			||||||
 | 
					      assetMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
 | 
					      searchMock.import.mockRejectedValue(new Error('import failed'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAssets();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleIndexAsset', () => {
 | 
				
			||||||
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAsset({ asset: assetEntityStub.image });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.index).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should index the asset', async () => {
 | 
				
			||||||
 | 
					      await sut.handleIndexAsset({ asset: assetEntityStub.image });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.index).toHaveBeenCalledWith('assets', assetEntityStub.image);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should log an error', async () => {
 | 
				
			||||||
 | 
					      searchMock.index.mockRejectedValue(new Error('index failed'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAsset({ asset: assetEntityStub.image });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.index).toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleIndexAlbums', () => {
 | 
				
			||||||
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAlbums();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.import).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should index all the albums', async () => {
 | 
				
			||||||
 | 
					      albumMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAlbums();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.import).toHaveBeenCalledWith('albums', [], true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should log an error', async () => {
 | 
				
			||||||
 | 
					      albumMock.getAll.mockResolvedValue([]);
 | 
				
			||||||
 | 
					      searchMock.import.mockRejectedValue(new Error('import failed'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAlbums();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleIndexAlbum', () => {
 | 
				
			||||||
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAlbum({ album: albumStub.empty });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.index).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should index the album', async () => {
 | 
				
			||||||
 | 
					      await sut.handleIndexAlbum({ album: albumStub.empty });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.index).toHaveBeenCalledWith('albums', albumStub.empty);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should log an error', async () => {
 | 
				
			||||||
 | 
					      searchMock.index.mockRejectedValue(new Error('index failed'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleIndexAlbum({ album: albumStub.empty });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.index).toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleRemoveAlbum', () => {
 | 
				
			||||||
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleRemoveAlbum({ id: 'album1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.delete).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should remove the album', async () => {
 | 
				
			||||||
 | 
					      await sut.handleRemoveAlbum({ id: 'album1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.delete).toHaveBeenCalledWith('albums', 'album1');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should log an error', async () => {
 | 
				
			||||||
 | 
					      searchMock.delete.mockRejectedValue(new Error('remove failed'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleRemoveAlbum({ id: 'album1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.delete).toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleRemoveAsset', () => {
 | 
				
			||||||
 | 
					    it('should skip if search is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.get.mockReturnValue('false');
 | 
				
			||||||
 | 
					      sut = new SearchService(albumMock, assetMock, jobMock, searchMock, configMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleRemoveAsset({ id: 'asset1`' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.delete).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should remove the asset', async () => {
 | 
				
			||||||
 | 
					      await sut.handleRemoveAsset({ id: 'asset1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.delete).toHaveBeenCalledWith('assets', 'asset1');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should log an error', async () => {
 | 
				
			||||||
 | 
					      searchMock.delete.mockRejectedValue(new Error('remove failed'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleRemoveAsset({ id: 'asset1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(searchMock.delete).toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										154
									
								
								server/libs/domain/src/search/search.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								server/libs/domain/src/search/search.service.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { ConfigService } from '@nestjs/config';
 | 
				
			||||||
 | 
					import { IAlbumRepository } from '../album/album.repository';
 | 
				
			||||||
 | 
					import { IAssetRepository } from '../asset/asset.repository';
 | 
				
			||||||
 | 
					import { AuthUserDto } from '../auth';
 | 
				
			||||||
 | 
					import { IAlbumJob, IAssetJob, IDeleteJob, IJobRepository, JobName } from '../job';
 | 
				
			||||||
 | 
					import { SearchDto } from './dto';
 | 
				
			||||||
 | 
					import { SearchConfigResponseDto, SearchResponseDto } from './response-dto';
 | 
				
			||||||
 | 
					import { ISearchRepository, SearchCollection } from './search.repository';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class SearchService {
 | 
				
			||||||
 | 
					  private logger = new Logger(SearchService.name);
 | 
				
			||||||
 | 
					  private enabled: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    @Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
 | 
				
			||||||
 | 
					    @Inject(IAssetRepository) private assetRepository: IAssetRepository,
 | 
				
			||||||
 | 
					    @Inject(IJobRepository) private jobRepository: IJobRepository,
 | 
				
			||||||
 | 
					    @Inject(ISearchRepository) private searchRepository: ISearchRepository,
 | 
				
			||||||
 | 
					    configService: ConfigService,
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.enabled = configService.get('TYPESENSE_ENABLED') !== 'false';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isEnabled() {
 | 
				
			||||||
 | 
					    return this.enabled;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getConfig(): SearchConfigResponseDto {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      enabled: this.enabled,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async bootstrap() {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.logger.log('Running bootstrap');
 | 
				
			||||||
 | 
					    await this.searchRepository.setup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const migrationStatus = await this.searchRepository.checkMigrationStatus();
 | 
				
			||||||
 | 
					    if (migrationStatus[SearchCollection.ASSETS]) {
 | 
				
			||||||
 | 
					      this.logger.debug('Queueing job to re-index all assets');
 | 
				
			||||||
 | 
					      await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSETS });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (migrationStatus[SearchCollection.ALBUMS]) {
 | 
				
			||||||
 | 
					      this.logger.debug('Queueing job to re-index all albums');
 | 
				
			||||||
 | 
					      await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUMS });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async search(authUser: AuthUserDto, dto: SearchDto): Promise<SearchResponseDto> {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Search is disabled');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const query = dto.query || '*';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      assets: (await this.searchRepository.search(SearchCollection.ASSETS, query, {
 | 
				
			||||||
 | 
					        userId: authUser.id,
 | 
				
			||||||
 | 
					        ...dto,
 | 
				
			||||||
 | 
					      })) as any,
 | 
				
			||||||
 | 
					      albums: (await this.searchRepository.search(SearchCollection.ALBUMS, query, {
 | 
				
			||||||
 | 
					        userId: authUser.id,
 | 
				
			||||||
 | 
					        ...dto,
 | 
				
			||||||
 | 
					      })) as any,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleIndexAssets() {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      this.logger.debug(`Running indexAssets`);
 | 
				
			||||||
 | 
					      // TODO: do this in batches based on searchIndexVersion
 | 
				
			||||||
 | 
					      const assets = await this.assetRepository.getAll({ isVisible: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.logger.log(`Indexing ${assets.length} assets`);
 | 
				
			||||||
 | 
					      await this.searchRepository.import(SearchCollection.ASSETS, assets, true);
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      this.logger.error(`Unable to index all assets`, error?.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleIndexAsset(data: IAssetJob) {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { asset } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.searchRepository.index(SearchCollection.ASSETS, asset);
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      this.logger.error(`Unable to index asset: ${asset.id}`, error?.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleIndexAlbums() {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const albums = await this.albumRepository.getAll();
 | 
				
			||||||
 | 
					      this.logger.log(`Indexing ${albums.length} albums`);
 | 
				
			||||||
 | 
					      await this.searchRepository.import(SearchCollection.ALBUMS, albums, true);
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      this.logger.error(`Unable to index all albums`, error?.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleIndexAlbum(data: IAlbumJob) {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { album } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.searchRepository.index(SearchCollection.ALBUMS, album);
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      this.logger.error(`Unable to index album: ${album.id}`, error?.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleRemoveAlbum(data: IDeleteJob) {
 | 
				
			||||||
 | 
					    await this.handleRemove(SearchCollection.ALBUMS, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async handleRemoveAsset(data: IDeleteJob) {
 | 
				
			||||||
 | 
					    await this.handleRemove(SearchCollection.ASSETS, data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async handleRemove(collection: SearchCollection, data: IDeleteJob) {
 | 
				
			||||||
 | 
					    if (!this.enabled) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { id } = data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await this.searchRepository.delete(collection, id);
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      this.logger.error(`Unable to remove ${collection}: ${id}`, error?.stack);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,5 +3,7 @@ import { IAlbumRepository } from '../src';
 | 
				
			|||||||
export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
 | 
					export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    deleteAll: jest.fn(),
 | 
					    deleteAll: jest.fn(),
 | 
				
			||||||
 | 
					    getAll: jest.fn(),
 | 
				
			||||||
 | 
					    save: jest.fn(),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  AlbumEntity,
 | 
				
			||||||
  APIKeyEntity,
 | 
					  APIKeyEntity,
 | 
				
			||||||
  AssetEntity,
 | 
					  AssetEntity,
 | 
				
			||||||
  AssetType,
 | 
					  AssetType,
 | 
				
			||||||
@ -155,6 +156,21 @@ export const assetEntityStub = {
 | 
				
			|||||||
  } as AssetEntity),
 | 
					  } as AssetEntity),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const albumStub = {
 | 
				
			||||||
 | 
					  empty: Object.freeze<AlbumEntity>({
 | 
				
			||||||
 | 
					    id: 'album-1',
 | 
				
			||||||
 | 
					    albumName: 'Empty album',
 | 
				
			||||||
 | 
					    ownerId: authStub.admin.id,
 | 
				
			||||||
 | 
					    owner: userEntityStub.admin,
 | 
				
			||||||
 | 
					    assets: [],
 | 
				
			||||||
 | 
					    albumThumbnailAssetId: null,
 | 
				
			||||||
 | 
					    createdAt: new Date().toISOString(),
 | 
				
			||||||
 | 
					    updatedAt: new Date().toISOString(),
 | 
				
			||||||
 | 
					    sharedLinks: [],
 | 
				
			||||||
 | 
					    sharedUsers: [],
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const assetInfo: ExifResponseDto = {
 | 
					const assetInfo: ExifResponseDto = {
 | 
				
			||||||
  make: 'camera-make',
 | 
					  make: 'camera-make',
 | 
				
			||||||
  model: 'camera-model',
 | 
					  model: 'camera-model',
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ export * from './device-info.repository.mock';
 | 
				
			|||||||
export * from './fixtures';
 | 
					export * from './fixtures';
 | 
				
			||||||
export * from './job.repository.mock';
 | 
					export * from './job.repository.mock';
 | 
				
			||||||
export * from './machine-learning.repository.mock';
 | 
					export * from './machine-learning.repository.mock';
 | 
				
			||||||
 | 
					export * from './search.repository.mock';
 | 
				
			||||||
export * from './shared-link.repository.mock';
 | 
					export * from './shared-link.repository.mock';
 | 
				
			||||||
export * from './smart-info.repository.mock';
 | 
					export * from './smart-info.repository.mock';
 | 
				
			||||||
export * from './storage.repository.mock';
 | 
					export * from './storage.repository.mock';
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								server/libs/domain/test/search.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/libs/domain/test/search.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { ISearchRepository } from '../src';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    setup: jest.fn(),
 | 
				
			||||||
 | 
					    checkMigrationStatus: jest.fn(),
 | 
				
			||||||
 | 
					    index: jest.fn(),
 | 
				
			||||||
 | 
					    import: jest.fn(),
 | 
				
			||||||
 | 
					    search: jest.fn(),
 | 
				
			||||||
 | 
					    delete: jest.fn(),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -11,4 +11,13 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
  async deleteAll(userId: string): Promise<void> {
 | 
					  async deleteAll(userId: string): Promise<void> {
 | 
				
			||||||
    await this.repository.delete({ ownerId: userId });
 | 
					    await this.repository.delete({ ownerId: userId });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAll(): Promise<AlbumEntity[]> {
 | 
				
			||||||
 | 
					    return this.repository.find();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async save(album: Partial<AlbumEntity>) {
 | 
				
			||||||
 | 
					    const { id } = await this.repository.save(album);
 | 
				
			||||||
 | 
					    return this.repository.findOneOrFail({ where: { id } });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { IAssetRepository } from '@app/domain';
 | 
					import { AssetSearchOptions, IAssetRepository } from '@app/domain';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Not, Repository } from 'typeorm';
 | 
					import { Not, Repository } from 'typeorm';
 | 
				
			||||||
@ -12,13 +12,32 @@ export class AssetRepository implements IAssetRepository {
 | 
				
			|||||||
    await this.repository.delete({ ownerId });
 | 
					    await this.repository.delete({ ownerId });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getAll(): Promise<AssetEntity[]> {
 | 
					  getAll(options?: AssetSearchOptions | undefined): Promise<AssetEntity[]> {
 | 
				
			||||||
    return this.repository.find({ relations: { exifInfo: true } });
 | 
					    options = options || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.repository.find({
 | 
				
			||||||
 | 
					      where: {
 | 
				
			||||||
 | 
					        isVisible: options.isVisible,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        exifInfo: true,
 | 
				
			||||||
 | 
					        smartInfo: true,
 | 
				
			||||||
 | 
					        tags: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async save(asset: Partial<AssetEntity>): Promise<AssetEntity> {
 | 
					  async save(asset: Partial<AssetEntity>): Promise<AssetEntity> {
 | 
				
			||||||
    const { id } = await this.repository.save(asset);
 | 
					    const { id } = await this.repository.save(asset);
 | 
				
			||||||
    return this.repository.findOneOrFail({ where: { id } });
 | 
					    return this.repository.findOneOrFail({
 | 
				
			||||||
 | 
					      where: { id },
 | 
				
			||||||
 | 
					      relations: {
 | 
				
			||||||
 | 
					        exifInfo: true,
 | 
				
			||||||
 | 
					        owner: true,
 | 
				
			||||||
 | 
					        smartInfo: true,
 | 
				
			||||||
 | 
					        tags: true,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null> {
 | 
					  findLivePhotoMatch(livePhotoCID: string, otherAssetId: string, type: AssetType): Promise<AssetEntity | null> {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  IKeyRepository,
 | 
					  IKeyRepository,
 | 
				
			||||||
  IMachineLearningRepository,
 | 
					  IMachineLearningRepository,
 | 
				
			||||||
  IMediaRepository,
 | 
					  IMediaRepository,
 | 
				
			||||||
 | 
					  ISearchRepository,
 | 
				
			||||||
  ISharedLinkRepository,
 | 
					  ISharedLinkRepository,
 | 
				
			||||||
  ISmartInfoRepository,
 | 
					  ISmartInfoRepository,
 | 
				
			||||||
  IStorageRepository,
 | 
					  IStorageRepository,
 | 
				
			||||||
@ -45,6 +46,7 @@ import {
 | 
				
			|||||||
import { JobRepository } from './job';
 | 
					import { JobRepository } from './job';
 | 
				
			||||||
import { MachineLearningRepository } from './machine-learning';
 | 
					import { MachineLearningRepository } from './machine-learning';
 | 
				
			||||||
import { MediaRepository } from './media';
 | 
					import { MediaRepository } from './media';
 | 
				
			||||||
 | 
					import { TypesenseRepository } from './search';
 | 
				
			||||||
import { FilesystemProvider } from './storage';
 | 
					import { FilesystemProvider } from './storage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const providers: Provider[] = [
 | 
					const providers: Provider[] = [
 | 
				
			||||||
@ -52,12 +54,12 @@ const providers: Provider[] = [
 | 
				
			|||||||
  { provide: IAssetRepository, useClass: AssetRepository },
 | 
					  { provide: IAssetRepository, useClass: AssetRepository },
 | 
				
			||||||
  { provide: ICommunicationRepository, useClass: CommunicationRepository },
 | 
					  { provide: ICommunicationRepository, useClass: CommunicationRepository },
 | 
				
			||||||
  { provide: ICryptoRepository, useClass: CryptoRepository },
 | 
					  { provide: ICryptoRepository, useClass: CryptoRepository },
 | 
				
			||||||
  { provide: ICryptoRepository, useClass: CryptoRepository },
 | 
					 | 
				
			||||||
  { provide: IDeviceInfoRepository, useClass: DeviceInfoRepository },
 | 
					  { provide: IDeviceInfoRepository, useClass: DeviceInfoRepository },
 | 
				
			||||||
  { provide: IKeyRepository, useClass: APIKeyRepository },
 | 
					  { provide: IKeyRepository, useClass: APIKeyRepository },
 | 
				
			||||||
  { provide: IJobRepository, useClass: JobRepository },
 | 
					  { provide: IJobRepository, useClass: JobRepository },
 | 
				
			||||||
  { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
 | 
					  { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
 | 
				
			||||||
  { provide: IMediaRepository, useClass: MediaRepository },
 | 
					  { provide: IMediaRepository, useClass: MediaRepository },
 | 
				
			||||||
 | 
					  { provide: ISearchRepository, useClass: TypesenseRepository },
 | 
				
			||||||
  { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
 | 
					  { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
 | 
				
			||||||
  { provide: ISmartInfoRepository, useClass: SmartInfoRepository },
 | 
					  { provide: ISmartInfoRepository, useClass: SmartInfoRepository },
 | 
				
			||||||
  { provide: IStorageRepository, useClass: FilesystemProvider },
 | 
					  { provide: IStorageRepository, useClass: FilesystemProvider },
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ export class JobRepository implements IJobRepository {
 | 
				
			|||||||
    @InjectQueue(QueueName.STORAGE_TEMPLATE_MIGRATION) private storageTemplateMigration: Queue,
 | 
					    @InjectQueue(QueueName.STORAGE_TEMPLATE_MIGRATION) private storageTemplateMigration: Queue,
 | 
				
			||||||
    @InjectQueue(QueueName.THUMBNAIL_GENERATION) private thumbnail: Queue,
 | 
					    @InjectQueue(QueueName.THUMBNAIL_GENERATION) private thumbnail: Queue,
 | 
				
			||||||
    @InjectQueue(QueueName.VIDEO_CONVERSION) private videoTranscode: Queue<IAssetJob>,
 | 
					    @InjectQueue(QueueName.VIDEO_CONVERSION) private videoTranscode: Queue<IAssetJob>,
 | 
				
			||||||
 | 
					    @InjectQueue(QueueName.SEARCH) private searchIndex: Queue,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async isActive(name: QueueName): Promise<boolean> {
 | 
					  async isActive(name: QueueName): Promise<boolean> {
 | 
				
			||||||
@ -70,6 +71,18 @@ export class JobRepository implements IJobRepository {
 | 
				
			|||||||
        await this.videoTranscode.add(item.name, item.data);
 | 
					        await this.videoTranscode.add(item.name, item.data);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case JobName.SEARCH_INDEX_ASSETS:
 | 
				
			||||||
 | 
					      case JobName.SEARCH_INDEX_ALBUMS:
 | 
				
			||||||
 | 
					        await this.searchIndex.add(item.name);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      case JobName.SEARCH_INDEX_ASSET:
 | 
				
			||||||
 | 
					      case JobName.SEARCH_INDEX_ALBUM:
 | 
				
			||||||
 | 
					      case JobName.SEARCH_REMOVE_ALBUM:
 | 
				
			||||||
 | 
					      case JobName.SEARCH_REMOVE_ASSET:
 | 
				
			||||||
 | 
					        await this.searchIndex.add(item.name, item.data);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        // TODO inject remaining queues and map job to queue
 | 
					        // TODO inject remaining queues and map job to queue
 | 
				
			||||||
        this.logger.error('Invalid job', item);
 | 
					        this.logger.error('Invalid job', item);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								server/libs/infra/src/search/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/libs/infra/src/search/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './typesense.repository';
 | 
				
			||||||
							
								
								
									
										13
									
								
								server/libs/infra/src/search/schemas/album.schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/libs/infra/src/search/schemas/album.schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const albumSchemaVersion = 1;
 | 
				
			||||||
 | 
					export const albumSchema: CollectionCreateSchema = {
 | 
				
			||||||
 | 
					  name: `albums-v${albumSchemaVersion}`,
 | 
				
			||||||
 | 
					  fields: [
 | 
				
			||||||
 | 
					    { name: 'ownerId', type: 'string', facet: false },
 | 
				
			||||||
 | 
					    { name: 'albumName', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					    { name: 'createdAt', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					    { name: 'updatedAt', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  default_sorting_field: 'createdAt',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										37
									
								
								server/libs/infra/src/search/schemas/asset.schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								server/libs/infra/src/search/schemas/asset.schema.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const assetSchemaVersion = 1;
 | 
				
			||||||
 | 
					export const assetSchema: CollectionCreateSchema = {
 | 
				
			||||||
 | 
					  name: `assets-v${assetSchemaVersion}`,
 | 
				
			||||||
 | 
					  fields: [
 | 
				
			||||||
 | 
					    // asset
 | 
				
			||||||
 | 
					    { name: 'ownerId', type: 'string', facet: false },
 | 
				
			||||||
 | 
					    { name: 'type', type: 'string', facet: true },
 | 
				
			||||||
 | 
					    { name: 'originalPath', type: 'string', facet: false },
 | 
				
			||||||
 | 
					    { name: 'createdAt', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					    { name: 'updatedAt', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					    { name: 'fileCreatedAt', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					    { name: 'fileModifiedAt', type: 'string', facet: false, sort: true },
 | 
				
			||||||
 | 
					    { name: 'isFavorite', type: 'bool', facet: true },
 | 
				
			||||||
 | 
					    // { name: 'checksum', type: 'string', facet: true },
 | 
				
			||||||
 | 
					    // { name: 'tags', type: 'string[]', facet: true, optional: true },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // exif
 | 
				
			||||||
 | 
					    { name: 'exifInfo.city', type: 'string', facet: true, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.country', type: 'string', facet: true, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.state', type: 'string', facet: true, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.description', type: 'string', facet: false, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.imageName', type: 'string', facet: false, optional: true },
 | 
				
			||||||
 | 
					    { name: 'geo', type: 'geopoint', facet: false, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.make', type: 'string', facet: true, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.model', type: 'string', facet: true, optional: true },
 | 
				
			||||||
 | 
					    { name: 'exifInfo.orientation', type: 'string', optional: true },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // smart info
 | 
				
			||||||
 | 
					    { name: 'smartInfo.objects', type: 'string[]', facet: true, optional: true },
 | 
				
			||||||
 | 
					    { name: 'smartInfo.tags', type: 'string[]', facet: true, optional: true },
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  token_separators: ['.'],
 | 
				
			||||||
 | 
					  enable_nested_fields: true,
 | 
				
			||||||
 | 
					  default_sorting_field: 'fileCreatedAt',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										325
									
								
								server/libs/infra/src/search/typesense.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								server/libs/infra/src/search/typesense.repository.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,325 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  ISearchRepository,
 | 
				
			||||||
 | 
					  SearchCollection,
 | 
				
			||||||
 | 
					  SearchCollectionIndexStatus,
 | 
				
			||||||
 | 
					  SearchFilter,
 | 
				
			||||||
 | 
					  SearchResult,
 | 
				
			||||||
 | 
					} from '@app/domain';
 | 
				
			||||||
 | 
					import { Injectable, Logger } from '@nestjs/common';
 | 
				
			||||||
 | 
					import _, { Dictionary } from 'lodash';
 | 
				
			||||||
 | 
					import { Client } from 'typesense';
 | 
				
			||||||
 | 
					import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
 | 
				
			||||||
 | 
					import { DocumentSchema, SearchResponse } from 'typesense/lib/Typesense/Documents';
 | 
				
			||||||
 | 
					import { AlbumEntity, AssetEntity } from '../db';
 | 
				
			||||||
 | 
					import { albumSchema } from './schemas/album.schema';
 | 
				
			||||||
 | 
					import { assetSchema } from './schemas/asset.schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface GeoAssetEntity extends AssetEntity {
 | 
				
			||||||
 | 
					  geo?: [number, number];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function removeNil<T extends Dictionary<any>>(item: T): Partial<T> {
 | 
				
			||||||
 | 
					  _.forOwn(item, (value, key) => {
 | 
				
			||||||
 | 
					    if (_.isNil(value) || (_.isObject(value) && !_.isDate(value) && _.isEmpty(removeNil(value)))) {
 | 
				
			||||||
 | 
					      delete item[key];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return item;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const schemaMap: Record<SearchCollection, CollectionCreateSchema> = {
 | 
				
			||||||
 | 
					  [SearchCollection.ASSETS]: assetSchema,
 | 
				
			||||||
 | 
					  [SearchCollection.ALBUMS]: albumSchema,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const schemas = Object.entries(schemaMap) as [SearchCollection, CollectionCreateSchema][];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SearchUpdateQueue<T = any> {
 | 
				
			||||||
 | 
					  upsert: T[];
 | 
				
			||||||
 | 
					  delete: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable()
 | 
				
			||||||
 | 
					export class TypesenseRepository implements ISearchRepository {
 | 
				
			||||||
 | 
					  private logger = new Logger(TypesenseRepository.name);
 | 
				
			||||||
 | 
					  private queue: Record<SearchCollection, SearchUpdateQueue> = {
 | 
				
			||||||
 | 
					    [SearchCollection.ASSETS]: {
 | 
				
			||||||
 | 
					      upsert: [],
 | 
				
			||||||
 | 
					      delete: [],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [SearchCollection.ALBUMS]: {
 | 
				
			||||||
 | 
					      upsert: [],
 | 
				
			||||||
 | 
					      delete: [],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _client: Client | null = null;
 | 
				
			||||||
 | 
					  private get client(): Client {
 | 
				
			||||||
 | 
					    if (!this._client) {
 | 
				
			||||||
 | 
					      throw new Error('Typesense client not available (no apiKey was provided)');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this._client;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    const apiKey = process.env.TYPESENSE_API_KEY;
 | 
				
			||||||
 | 
					    if (!apiKey) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this._client = new Client({
 | 
				
			||||||
 | 
					      nodes: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          host: process.env.TYPESENSE_HOST || 'typesense',
 | 
				
			||||||
 | 
					          port: Number(process.env.TYPESENSE_PORT) || 8108,
 | 
				
			||||||
 | 
					          protocol: process.env.TYPESENSE_PROTOCOL || 'http',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      apiKey,
 | 
				
			||||||
 | 
					      numRetries: 3,
 | 
				
			||||||
 | 
					      connectionTimeoutSeconds: 10,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setInterval(() => this.flush(), 5_000);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async setup(): Promise<void> {
 | 
				
			||||||
 | 
					    // upsert collections
 | 
				
			||||||
 | 
					    for (const [collectionName, schema] of schemas) {
 | 
				
			||||||
 | 
					      const collection = await this.client
 | 
				
			||||||
 | 
					        .collections(schema.name)
 | 
				
			||||||
 | 
					        .retrieve()
 | 
				
			||||||
 | 
					        .catch(() => null);
 | 
				
			||||||
 | 
					      if (!collection) {
 | 
				
			||||||
 | 
					        this.logger.log(`Creating schema: ${collectionName}/${schema.name}`);
 | 
				
			||||||
 | 
					        await this.client.collections().create(schema);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.logger.log(`Schema up to date: ${collectionName}/${schema.name}`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async checkMigrationStatus(): Promise<SearchCollectionIndexStatus> {
 | 
				
			||||||
 | 
					    const migrationMap: SearchCollectionIndexStatus = {
 | 
				
			||||||
 | 
					      [SearchCollection.ASSETS]: false,
 | 
				
			||||||
 | 
					      [SearchCollection.ALBUMS]: false,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check if alias is using the current schema
 | 
				
			||||||
 | 
					    const { aliases } = await this.client.aliases().retrieve();
 | 
				
			||||||
 | 
					    this.logger.log(`Alias mapping: ${JSON.stringify(aliases)}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const [aliasName, schema] of schemas) {
 | 
				
			||||||
 | 
					      const match = aliases.find((alias) => alias.name === aliasName);
 | 
				
			||||||
 | 
					      if (!match || match.collection_name !== schema.name) {
 | 
				
			||||||
 | 
					        migrationMap[aliasName] = true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.logger.log(`Collections needing migration: ${JSON.stringify(migrationMap)}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return migrationMap;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async index(collection: SearchCollection, item: AssetEntity | AlbumEntity, immediate?: boolean): Promise<void> {
 | 
				
			||||||
 | 
					    const schema = schemaMap[collection];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (collection === SearchCollection.ASSETS) {
 | 
				
			||||||
 | 
					      item = this.patchAsset(item as AssetEntity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (immediate) {
 | 
				
			||||||
 | 
					      await this.client.collections(schema.name).documents().upsert(item);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.queue[collection].upsert.push(item);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async delete(collection: SearchCollection, id: string, immediate?: boolean): Promise<void> {
 | 
				
			||||||
 | 
					    const schema = schemaMap[collection];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (immediate) {
 | 
				
			||||||
 | 
					      await this.client.collections(schema.name).documents().delete(id);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.queue[collection].delete.push(id);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async import(collection: SearchCollection, items: AssetEntity[] | AlbumEntity[], done: boolean): Promise<void> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const schema = schemaMap[collection];
 | 
				
			||||||
 | 
					      const _items = items.map((item) => {
 | 
				
			||||||
 | 
					        if (collection === SearchCollection.ASSETS) {
 | 
				
			||||||
 | 
					          item = this.patchAsset(item as AssetEntity);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // null values are invalid for typesense documents
 | 
				
			||||||
 | 
					        return removeNil(item);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (_items.length > 0) {
 | 
				
			||||||
 | 
					        await this.client
 | 
				
			||||||
 | 
					          .collections(schema.name)
 | 
				
			||||||
 | 
					          .documents()
 | 
				
			||||||
 | 
					          .import(_items, { action: 'upsert', dirty_values: 'coerce_or_drop' });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (done) {
 | 
				
			||||||
 | 
					        await this.updateAlias(collection);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (error: any) {
 | 
				
			||||||
 | 
					      this.handleError(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  search(collection: SearchCollection.ASSETS, query: string, filter: SearchFilter): Promise<SearchResult<AssetEntity>>;
 | 
				
			||||||
 | 
					  search(collection: SearchCollection.ALBUMS, query: string, filter: SearchFilter): Promise<SearchResult<AlbumEntity>>;
 | 
				
			||||||
 | 
					  async search(collection: SearchCollection, query: string, filters: SearchFilter) {
 | 
				
			||||||
 | 
					    const alias = await this.client.aliases(collection).retrieve();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { userId } = filters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const _filters = [`ownerId:${userId}`];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (filters.id) {
 | 
				
			||||||
 | 
					      _filters.push(`id:=${filters.id}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (collection === SearchCollection.ASSETS) {
 | 
				
			||||||
 | 
					      for (const item of schemaMap[collection].fields || []) {
 | 
				
			||||||
 | 
					        let value = filters[item.name as keyof SearchFilter];
 | 
				
			||||||
 | 
					        if (Array.isArray(value)) {
 | 
				
			||||||
 | 
					          value = `[${value.join(',')}]`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (item.facet && value !== undefined) {
 | 
				
			||||||
 | 
					          _filters.push(`${item.name}:${value}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.logger.debug(`Searching query='${query}', filters='${JSON.stringify(_filters)}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const results = await this.client
 | 
				
			||||||
 | 
					        .collections<AssetEntity>(alias.collection_name)
 | 
				
			||||||
 | 
					        .documents()
 | 
				
			||||||
 | 
					        .search({
 | 
				
			||||||
 | 
					          q: query,
 | 
				
			||||||
 | 
					          query_by: [
 | 
				
			||||||
 | 
					            'exifInfo.imageName',
 | 
				
			||||||
 | 
					            'exifInfo.country',
 | 
				
			||||||
 | 
					            'exifInfo.state',
 | 
				
			||||||
 | 
					            'exifInfo.city',
 | 
				
			||||||
 | 
					            'exifInfo.description',
 | 
				
			||||||
 | 
					            'smartInfo.tags',
 | 
				
			||||||
 | 
					            'smartInfo.objects',
 | 
				
			||||||
 | 
					          ].join(','),
 | 
				
			||||||
 | 
					          filter_by: _filters.join(' && '),
 | 
				
			||||||
 | 
					          per_page: 250,
 | 
				
			||||||
 | 
					          facet_by: (assetSchema.fields || [])
 | 
				
			||||||
 | 
					            .filter((field) => field.facet)
 | 
				
			||||||
 | 
					            .map((field) => field.name)
 | 
				
			||||||
 | 
					            .join(','),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return this.asResponse(results);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (collection === SearchCollection.ALBUMS) {
 | 
				
			||||||
 | 
					      const results = await this.client
 | 
				
			||||||
 | 
					        .collections<AlbumEntity>(alias.collection_name)
 | 
				
			||||||
 | 
					        .documents()
 | 
				
			||||||
 | 
					        .search({
 | 
				
			||||||
 | 
					          q: query,
 | 
				
			||||||
 | 
					          query_by: 'albumName',
 | 
				
			||||||
 | 
					          filter_by: _filters.join(','),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return this.asResponse(results);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    throw new Error(`Invalid collection: ${collection}`);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private asResponse<T extends DocumentSchema>(results: SearchResponse<T>): SearchResult<T> {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      page: results.page,
 | 
				
			||||||
 | 
					      total: results.found,
 | 
				
			||||||
 | 
					      count: results.out_of,
 | 
				
			||||||
 | 
					      items: (results.hits || []).map((hit) => hit.document),
 | 
				
			||||||
 | 
					      facets: (results.facet_counts || []).map((facet) => ({
 | 
				
			||||||
 | 
					        counts: facet.counts.map((item) => ({ count: item.count, value: item.value })),
 | 
				
			||||||
 | 
					        fieldName: facet.field_name as string,
 | 
				
			||||||
 | 
					      })),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async flush() {
 | 
				
			||||||
 | 
					    for (const [collection, schema] of schemas) {
 | 
				
			||||||
 | 
					      if (this.queue[collection].upsert.length > 0) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const items = this.queue[collection].upsert.map((item) => removeNil(item));
 | 
				
			||||||
 | 
					          this.logger.debug(`Flushing ${items.length} ${collection} upserts to typesense`);
 | 
				
			||||||
 | 
					          await this.client
 | 
				
			||||||
 | 
					            .collections(schema.name)
 | 
				
			||||||
 | 
					            .documents()
 | 
				
			||||||
 | 
					            .import(items, { action: 'upsert', dirty_values: 'coerce_or_drop' });
 | 
				
			||||||
 | 
					          this.queue[collection].upsert = [];
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          this.handleError(error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.queue[collection].delete.length > 0) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const items = this.queue[collection].delete;
 | 
				
			||||||
 | 
					          this.logger.debug(`Flushing ${items.length} ${collection} deletes to typesense`);
 | 
				
			||||||
 | 
					          await this.client
 | 
				
			||||||
 | 
					            .collections(schema.name)
 | 
				
			||||||
 | 
					            .documents()
 | 
				
			||||||
 | 
					            .delete({ filter_by: `id: [${items.join(',')}]` });
 | 
				
			||||||
 | 
					          this.queue[collection].delete = [];
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          this.handleError(error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private handleError(error: any): never {
 | 
				
			||||||
 | 
					    this.logger.error('Unable to index documents');
 | 
				
			||||||
 | 
					    const results = error.importResults || [];
 | 
				
			||||||
 | 
					    for (const result of results) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        result.document = JSON.parse(result.document);
 | 
				
			||||||
 | 
					      } catch {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.logger.verbose(JSON.stringify(results, null, 2));
 | 
				
			||||||
 | 
					    throw error;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async updateAlias(collection: SearchCollection) {
 | 
				
			||||||
 | 
					    const schema = schemaMap[collection];
 | 
				
			||||||
 | 
					    const alias = await this.client
 | 
				
			||||||
 | 
					      .aliases(collection)
 | 
				
			||||||
 | 
					      .retrieve()
 | 
				
			||||||
 | 
					      .catch(() => null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update alias to current collection
 | 
				
			||||||
 | 
					    this.logger.log(`Using new schema: ${alias?.collection_name || '(unset)'} => ${schema.name}`);
 | 
				
			||||||
 | 
					    await this.client.aliases().upsert(collection, { collection_name: schema.name });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // delete previous collection
 | 
				
			||||||
 | 
					    if (alias && alias.collection_name !== schema.name) {
 | 
				
			||||||
 | 
					      this.logger.log(`Deleting old schema: ${alias.collection_name}`);
 | 
				
			||||||
 | 
					      await this.client.collections(alias.collection_name).delete();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private patchAsset(asset: AssetEntity): GeoAssetEntity {
 | 
				
			||||||
 | 
					    const lat = asset.exifInfo?.latitude;
 | 
				
			||||||
 | 
					    const lng = asset.exifInfo?.longitude;
 | 
				
			||||||
 | 
					    if (lat && lng && lat !== 0 && lng !== 0) {
 | 
				
			||||||
 | 
					      return { ...asset, geo: [lat, lng] };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return asset;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										73
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										73
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -6,9 +6,10 @@
 | 
				
			|||||||
  "packages": {
 | 
					  "packages": {
 | 
				
			||||||
    "": {
 | 
					    "": {
 | 
				
			||||||
      "name": "immich",
 | 
					      "name": "immich",
 | 
				
			||||||
      "version": "1.49.0",
 | 
					      "version": "1.50.1",
 | 
				
			||||||
      "license": "UNLICENSED",
 | 
					      "license": "UNLICENSED",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@babel/runtime": "^7.20.13",
 | 
				
			||||||
        "@nestjs/bull": "^0.6.2",
 | 
					        "@nestjs/bull": "^0.6.2",
 | 
				
			||||||
        "@nestjs/common": "^9.2.1",
 | 
					        "@nestjs/common": "^9.2.1",
 | 
				
			||||||
        "@nestjs/config": "^2.2.0",
 | 
					        "@nestjs/config": "^2.2.0",
 | 
				
			||||||
@ -46,7 +47,8 @@
 | 
				
			|||||||
        "rxjs": "^7.2.0",
 | 
					        "rxjs": "^7.2.0",
 | 
				
			||||||
        "sanitize-filename": "^1.6.3",
 | 
					        "sanitize-filename": "^1.6.3",
 | 
				
			||||||
        "sharp": "^0.28.0",
 | 
					        "sharp": "^0.28.0",
 | 
				
			||||||
        "typeorm": "^0.3.11"
 | 
					        "typeorm": "^0.3.11",
 | 
				
			||||||
 | 
					        "typesense": "^1.5.2"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "immich": "bin/cli.sh"
 | 
					        "immich": "bin/cli.sh"
 | 
				
			||||||
@ -765,6 +767,17 @@
 | 
				
			|||||||
        "@babel/core": "^7.0.0-0"
 | 
					        "@babel/core": "^7.0.0-0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@babel/runtime": {
 | 
				
			||||||
 | 
					      "version": "7.20.13",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "regenerator-runtime": "^0.13.11"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=6.9.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@babel/template": {
 | 
					    "node_modules/@babel/template": {
 | 
				
			||||||
      "version": "7.16.7",
 | 
					      "version": "7.16.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
 | 
				
			||||||
@ -8104,6 +8117,18 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
					        "url": "https://github.com/sponsors/sindresorhus"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/loglevel": {
 | 
				
			||||||
 | 
					      "version": "1.8.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 0.6.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "tidelift",
 | 
				
			||||||
 | 
					        "url": "https://tidelift.com/funding/github/npm/loglevel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/lru-cache": {
 | 
					    "node_modules/lru-cache": {
 | 
				
			||||||
      "version": "6.0.0",
 | 
					      "version": "6.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
 | 
				
			||||||
@ -9498,6 +9523,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
 | 
				
			||||||
      "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
 | 
					      "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/regenerator-runtime": {
 | 
				
			||||||
 | 
					      "version": "0.13.11",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/regexpp": {
 | 
					    "node_modules/regexpp": {
 | 
				
			||||||
      "version": "3.2.0",
 | 
					      "version": "3.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
 | 
				
			||||||
@ -11106,6 +11136,18 @@
 | 
				
			|||||||
        "node": ">=4.2.0"
 | 
					        "node": ">=4.2.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/typesense": {
 | 
				
			||||||
 | 
					      "version": "1.5.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/typesense/-/typesense-1.5.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ysARFw+4z3AdSViOACqf7K9TXoP2wAXd5p5uSGTdXW14UYjcEzpV/S/EhMoiC6YdZyrnbDdNsxgWbf+AWJ9Udw==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "axios": "^0.26.0",
 | 
				
			||||||
 | 
					        "loglevel": "^1.8.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "@babel/runtime": "^7.17.2"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/uglify-js": {
 | 
					    "node_modules/uglify-js": {
 | 
				
			||||||
      "version": "3.17.4",
 | 
					      "version": "3.17.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
 | 
				
			||||||
@ -12115,6 +12157,14 @@
 | 
				
			|||||||
        "@babel/helper-plugin-utils": "^7.16.7"
 | 
					        "@babel/helper-plugin-utils": "^7.16.7"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@babel/runtime": {
 | 
				
			||||||
 | 
					      "version": "7.20.13",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "regenerator-runtime": "^0.13.11"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@babel/template": {
 | 
					    "@babel/template": {
 | 
				
			||||||
      "version": "7.16.7",
 | 
					      "version": "7.16.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz",
 | 
				
			||||||
@ -17808,6 +17858,11 @@
 | 
				
			|||||||
        "is-unicode-supported": "^0.1.0"
 | 
					        "is-unicode-supported": "^0.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "loglevel": {
 | 
				
			||||||
 | 
					      "version": "1.8.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "lru-cache": {
 | 
					    "lru-cache": {
 | 
				
			||||||
      "version": "6.0.0",
 | 
					      "version": "6.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
 | 
				
			||||||
@ -18862,6 +18917,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
 | 
				
			||||||
      "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
 | 
					      "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "regenerator-runtime": {
 | 
				
			||||||
 | 
					      "version": "0.13.11",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "regexpp": {
 | 
					    "regexpp": {
 | 
				
			||||||
      "version": "3.2.0",
 | 
					      "version": "3.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
 | 
				
			||||||
@ -19962,6 +20022,15 @@
 | 
				
			|||||||
      "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
 | 
					      "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
 | 
				
			||||||
      "devOptional": true
 | 
					      "devOptional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "typesense": {
 | 
				
			||||||
 | 
					      "version": "1.5.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/typesense/-/typesense-1.5.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-ysARFw+4z3AdSViOACqf7K9TXoP2wAXd5p5uSGTdXW14UYjcEzpV/S/EhMoiC6YdZyrnbDdNsxgWbf+AWJ9Udw==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "axios": "^0.26.0",
 | 
				
			||||||
 | 
					        "loglevel": "^1.8.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "uglify-js": {
 | 
					    "uglify-js": {
 | 
				
			||||||
      "version": "3.17.4",
 | 
					      "version": "3.17.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -39,6 +39,7 @@
 | 
				
			|||||||
    "api:generate": "bash ./bin/generate-open-api.sh"
 | 
					    "api:generate": "bash ./bin/generate-open-api.sh"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@babel/runtime": "^7.20.13",
 | 
				
			||||||
    "@nestjs/bull": "^0.6.2",
 | 
					    "@nestjs/bull": "^0.6.2",
 | 
				
			||||||
    "@nestjs/common": "^9.2.1",
 | 
					    "@nestjs/common": "^9.2.1",
 | 
				
			||||||
    "@nestjs/config": "^2.2.0",
 | 
					    "@nestjs/config": "^2.2.0",
 | 
				
			||||||
@ -76,7 +77,8 @@
 | 
				
			|||||||
    "rxjs": "^7.2.0",
 | 
					    "rxjs": "^7.2.0",
 | 
				
			||||||
    "sanitize-filename": "^1.6.3",
 | 
					    "sanitize-filename": "^1.6.3",
 | 
				
			||||||
    "sharp": "^0.28.0",
 | 
					    "sharp": "^0.28.0",
 | 
				
			||||||
    "typeorm": "^0.3.11"
 | 
					    "typeorm": "^0.3.11",
 | 
				
			||||||
 | 
					    "typesense": "^1.5.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@nestjs/cli": "^9.1.8",
 | 
					    "@nestjs/cli": "^9.1.8",
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
	DeviceInfoApi,
 | 
						DeviceInfoApi,
 | 
				
			||||||
	JobApi,
 | 
						JobApi,
 | 
				
			||||||
	OAuthApi,
 | 
						OAuthApi,
 | 
				
			||||||
 | 
						SearchApi,
 | 
				
			||||||
	ServerInfoApi,
 | 
						ServerInfoApi,
 | 
				
			||||||
	ShareApi,
 | 
						ShareApi,
 | 
				
			||||||
	SystemConfigApi,
 | 
						SystemConfigApi,
 | 
				
			||||||
@ -21,6 +22,7 @@ export class ImmichApi {
 | 
				
			|||||||
	public authenticationApi: AuthenticationApi;
 | 
						public authenticationApi: AuthenticationApi;
 | 
				
			||||||
	public oauthApi: OAuthApi;
 | 
						public oauthApi: OAuthApi;
 | 
				
			||||||
	public deviceInfoApi: DeviceInfoApi;
 | 
						public deviceInfoApi: DeviceInfoApi;
 | 
				
			||||||
 | 
						public searchApi: SearchApi;
 | 
				
			||||||
	public serverInfoApi: ServerInfoApi;
 | 
						public serverInfoApi: ServerInfoApi;
 | 
				
			||||||
	public jobApi: JobApi;
 | 
						public jobApi: JobApi;
 | 
				
			||||||
	public keyApi: APIKeyApi;
 | 
						public keyApi: APIKeyApi;
 | 
				
			||||||
@ -41,6 +43,7 @@ export class ImmichApi {
 | 
				
			|||||||
		this.serverInfoApi = new ServerInfoApi(this.config);
 | 
							this.serverInfoApi = new ServerInfoApi(this.config);
 | 
				
			||||||
		this.jobApi = new JobApi(this.config);
 | 
							this.jobApi = new JobApi(this.config);
 | 
				
			||||||
		this.keyApi = new APIKeyApi(this.config);
 | 
							this.keyApi = new APIKeyApi(this.config);
 | 
				
			||||||
 | 
							this.searchApi = new SearchApi(this.config);
 | 
				
			||||||
		this.systemConfigApi = new SystemConfigApi(this.config);
 | 
							this.systemConfigApi = new SystemConfigApi(this.config);
 | 
				
			||||||
		this.shareApi = new ShareApi(this.config);
 | 
							this.shareApi = new ShareApi(this.config);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										374
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										374
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -1451,6 +1451,37 @@ export interface RemoveAssetsDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'assetIds': Array<string>;
 | 
					    'assetIds': Array<string>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchAlbumResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchAlbumResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof SearchAlbumResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'total': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof SearchAlbumResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'count': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<AlbumResponseDto>}
 | 
				
			||||||
 | 
					     * @memberof SearchAlbumResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'items': Array<AlbumResponseDto>;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<SearchFacetResponseDto>}
 | 
				
			||||||
 | 
					     * @memberof SearchAlbumResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'facets': Array<SearchFacetResponseDto>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -1464,6 +1495,107 @@ export interface SearchAssetDto {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    'searchTerm': string;
 | 
					    'searchTerm': string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchAssetResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchAssetResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof SearchAssetResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'total': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof SearchAssetResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'count': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<AssetResponseDto>}
 | 
				
			||||||
 | 
					     * @memberof SearchAssetResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'items': Array<AssetResponseDto>;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<SearchFacetResponseDto>}
 | 
				
			||||||
 | 
					     * @memberof SearchAssetResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'facets': Array<SearchFacetResponseDto>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchConfigResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchConfigResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {boolean}
 | 
				
			||||||
 | 
					     * @memberof SearchConfigResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'enabled': boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchFacetCountResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchFacetCountResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {number}
 | 
				
			||||||
 | 
					     * @memberof SearchFacetCountResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'count': number;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SearchFacetCountResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'value': string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchFacetResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchFacetResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {string}
 | 
				
			||||||
 | 
					     * @memberof SearchFacetResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'fieldName': string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {Array<SearchFacetCountResponseDto>}
 | 
				
			||||||
 | 
					     * @memberof SearchFacetResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'counts': Array<SearchFacetCountResponseDto>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @interface SearchResponseDto
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export interface SearchResponseDto {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {SearchAlbumResponseDto}
 | 
				
			||||||
 | 
					     * @memberof SearchResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'albums': SearchAlbumResponseDto;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @type {SearchAssetResponseDto}
 | 
				
			||||||
 | 
					     * @memberof SearchResponseDto
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    'assets': SearchAssetResponseDto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
@ -6485,6 +6617,248 @@ export class OAuthApi extends BaseAPI {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * SearchApi - axios parameter creator
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const SearchApiAxiosParamCreator = function (configuration?: Configuration) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getSearchConfig: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            const localVarPath = `/search/config`;
 | 
				
			||||||
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
 | 
					            let baseOptions;
 | 
				
			||||||
 | 
					            if (configuration) {
 | 
				
			||||||
 | 
					                baseOptions = configuration.baseOptions;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
				
			||||||
 | 
					            const localVarHeaderParameter = {} as any;
 | 
				
			||||||
 | 
					            const localVarQueryParameter = {} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication bearer required
 | 
				
			||||||
 | 
					            // http bearer authentication required
 | 
				
			||||||
 | 
					            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication cookie required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} [query] 
 | 
				
			||||||
 | 
					         * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] 
 | 
				
			||||||
 | 
					         * @param {boolean} [isFavorite] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoCity] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoState] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoCountry] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoMake] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoModel] 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        search: async (query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
				
			||||||
 | 
					            const localVarPath = `/search`;
 | 
				
			||||||
 | 
					            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
				
			||||||
 | 
					            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
				
			||||||
 | 
					            let baseOptions;
 | 
				
			||||||
 | 
					            if (configuration) {
 | 
				
			||||||
 | 
					                baseOptions = configuration.baseOptions;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
				
			||||||
 | 
					            const localVarHeaderParameter = {} as any;
 | 
				
			||||||
 | 
					            const localVarQueryParameter = {} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication bearer required
 | 
				
			||||||
 | 
					            // http bearer authentication required
 | 
				
			||||||
 | 
					            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // authentication cookie required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (query !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['query'] = query;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (type !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['type'] = type;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (isFavorite !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['isFavorite'] = isFavorite;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (exifInfoCity !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['exifInfo.city'] = exifInfoCity;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (exifInfoState !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['exifInfo.state'] = exifInfoState;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (exifInfoCountry !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['exifInfo.country'] = exifInfoCountry;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (exifInfoMake !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['exifInfo.make'] = exifInfoMake;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (exifInfoModel !== undefined) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['exifInfo.model'] = exifInfoModel;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (smartInfoObjects) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['smartInfo.objects'] = smartInfoObjects;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (smartInfoTags) {
 | 
				
			||||||
 | 
					                localVarQueryParameter['smartInfo.tags'] = smartInfoTags;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
				
			||||||
 | 
					            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
				
			||||||
 | 
					            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                url: toPathString(localVarUrlObj),
 | 
				
			||||||
 | 
					                options: localVarRequestOptions,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * SearchApi - functional programming interface
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const SearchApiFp = function(configuration?: Configuration) {
 | 
				
			||||||
 | 
					    const localVarAxiosParamCreator = SearchApiAxiosParamCreator(configuration)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async getSearchConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchConfigResponseDto>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.getSearchConfig(options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} [query] 
 | 
				
			||||||
 | 
					         * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] 
 | 
				
			||||||
 | 
					         * @param {boolean} [isFavorite] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoCity] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoState] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoCountry] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoMake] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoModel] 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        async search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SearchResponseDto>> {
 | 
				
			||||||
 | 
					            const localVarAxiosArgs = await localVarAxiosParamCreator.search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, options);
 | 
				
			||||||
 | 
					            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * SearchApi - factory interface
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const SearchApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
 | 
				
			||||||
 | 
					    const localVarFp = SearchApiFp(configuration)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        getSearchConfig(options?: any): AxiosPromise<SearchConfigResponseDto> {
 | 
				
			||||||
 | 
					            return localVarFp.getSearchConfig(options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        /**
 | 
				
			||||||
 | 
					         * 
 | 
				
			||||||
 | 
					         * @param {string} [query] 
 | 
				
			||||||
 | 
					         * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] 
 | 
				
			||||||
 | 
					         * @param {boolean} [isFavorite] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoCity] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoState] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoCountry] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoMake] 
 | 
				
			||||||
 | 
					         * @param {string} [exifInfoModel] 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
 | 
					         * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					         * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					         * @throws {RequiredError}
 | 
				
			||||||
 | 
					         */
 | 
				
			||||||
 | 
					        search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options?: any): AxiosPromise<SearchResponseDto> {
 | 
				
			||||||
 | 
					            return localVarFp.search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, options).then((request) => request(axios, basePath));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * SearchApi - object-oriented interface
 | 
				
			||||||
 | 
					 * @export
 | 
				
			||||||
 | 
					 * @class SearchApi
 | 
				
			||||||
 | 
					 * @extends {BaseAPI}
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class SearchApi extends BaseAPI {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof SearchApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public getSearchConfig(options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return SearchApiFp(this.configuration).getSearchConfig(options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * @param {string} [query] 
 | 
				
			||||||
 | 
					     * @param {'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER'} [type] 
 | 
				
			||||||
 | 
					     * @param {boolean} [isFavorite] 
 | 
				
			||||||
 | 
					     * @param {string} [exifInfoCity] 
 | 
				
			||||||
 | 
					     * @param {string} [exifInfoState] 
 | 
				
			||||||
 | 
					     * @param {string} [exifInfoCountry] 
 | 
				
			||||||
 | 
					     * @param {string} [exifInfoMake] 
 | 
				
			||||||
 | 
					     * @param {string} [exifInfoModel] 
 | 
				
			||||||
 | 
					     * @param {Array<string>} [smartInfoObjects] 
 | 
				
			||||||
 | 
					     * @param {Array<string>} [smartInfoTags] 
 | 
				
			||||||
 | 
					     * @param {*} [options] Override http request option.
 | 
				
			||||||
 | 
					     * @throws {RequiredError}
 | 
				
			||||||
 | 
					     * @memberof SearchApi
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public search(query?: string, type?: 'IMAGE' | 'VIDEO' | 'AUDIO' | 'OTHER', isFavorite?: boolean, exifInfoCity?: string, exifInfoState?: string, exifInfoCountry?: string, exifInfoMake?: string, exifInfoModel?: string, smartInfoObjects?: Array<string>, smartInfoTags?: Array<string>, options?: AxiosRequestConfig) {
 | 
				
			||||||
 | 
					        return SearchApiFp(this.configuration).search(query, type, isFavorite, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, smartInfoObjects, smartInfoTags, options).then((request) => request(this.axios, this.basePath));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * ServerInfoApi - axios parameter creator
 | 
					 * ServerInfoApi - axios parameter creator
 | 
				
			||||||
 * @export
 | 
					 * @export
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								web/src/app.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								web/src/app.d.ts
									
									
									
									
										vendored
									
									
								
							@ -13,7 +13,7 @@ declare namespace App {
 | 
				
			|||||||
	interface Error {
 | 
						interface Error {
 | 
				
			||||||
		message: string;
 | 
							message: string;
 | 
				
			||||||
		stack?: string;
 | 
							stack?: string;
 | 
				
			||||||
		code?: string;
 | 
							code?: string | number;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import type { Handle, HandleServerError } from '@sveltejs/kit';
 | 
					import type { Handle, HandleServerError } from '@sveltejs/kit';
 | 
				
			||||||
import { AxiosError } from 'axios';
 | 
					import { AxiosError, AxiosResponse } from 'axios';
 | 
				
			||||||
import { env } from '$env/dynamic/public';
 | 
					import { env } from '$env/dynamic/public';
 | 
				
			||||||
import { ImmichApi } from './api/api';
 | 
					import { ImmichApi } from './api/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -34,11 +34,24 @@ export const handle = (async ({ event, resolve }) => {
 | 
				
			|||||||
	return res;
 | 
						return res;
 | 
				
			||||||
}) satisfies Handle;
 | 
					}) satisfies Handle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const handleError: HandleServerError = async ({ error }) => {
 | 
					export const handleError: HandleServerError = async ({ error }) => {
 | 
				
			||||||
	const httpError = error as AxiosError;
 | 
						const httpError = error as AxiosError;
 | 
				
			||||||
 | 
						const response = httpError?.response as AxiosResponse<{
 | 
				
			||||||
 | 
							message: string;
 | 
				
			||||||
 | 
							statusCode: number;
 | 
				
			||||||
 | 
							error: string;
 | 
				
			||||||
 | 
						}>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let code = response?.data?.statusCode || response?.status || httpError.code || '500';
 | 
				
			||||||
 | 
						if (response) {
 | 
				
			||||||
 | 
							code += ` - ${response.data?.error || response.statusText}`;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		message: httpError?.message || 'Hmm, not sure about that. Check the logs or open a ticket?',
 | 
							message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
 | 
				
			||||||
		stack: httpError?.stack,
 | 
							code,
 | 
				
			||||||
		code: httpError.code || '500'
 | 
							stack: httpError?.stack
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	$: {
 | 
						$: {
 | 
				
			||||||
		if (assets.length < 6) {
 | 
							if (assets.length < 6) {
 | 
				
			||||||
			thumbnailSize = Math.floor(viewWidth / assets.length - assets.length);
 | 
								thumbnailSize = Math.min(320, Math.floor(viewWidth / assets.length - assets.length));
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
 | 
								if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
 | 
				
			||||||
			else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
 | 
								else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@
 | 
				
			|||||||
	import ImmichLogo from '../immich-logo.svelte';
 | 
						import ImmichLogo from '../immich-logo.svelte';
 | 
				
			||||||
	export let user: UserResponseDto;
 | 
						export let user: UserResponseDto;
 | 
				
			||||||
	export let shouldShowUploadButton = true;
 | 
						export let shouldShowUploadButton = true;
 | 
				
			||||||
 | 
						export let term = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let shouldShowAccountInfo = false;
 | 
						let shouldShowAccountInfo = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,6 +36,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		goto(data.redirectUri || '/auth/login?autoLaunch=0');
 | 
							goto(data.redirectUri || '/auth/login?autoLaunch=0');
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const onSearch = () => {
 | 
				
			||||||
 | 
							goto(`/search?q=${term}`);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section
 | 
					<section
 | 
				
			||||||
@ -52,12 +57,16 @@
 | 
				
			|||||||
				IMMICH
 | 
									IMMICH
 | 
				
			||||||
			</h1>
 | 
								</h1>
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
		<div class="flex-1 ml-24">
 | 
							<form class="flex-1 ml-24" autocomplete="off" on:submit|preventDefault={onSearch}>
 | 
				
			||||||
			<input
 | 
								<input
 | 
				
			||||||
 | 
									type="text"
 | 
				
			||||||
 | 
									name="search"
 | 
				
			||||||
				class="w-[50%] rounded-3xl bg-gray-200 dark:bg-immich-dark-gray  px-8 py-4"
 | 
									class="w-[50%] rounded-3xl bg-gray-200 dark:bg-immich-dark-gray  px-8 py-4"
 | 
				
			||||||
				placeholder="Search - Coming soon"
 | 
									placeholder="Search"
 | 
				
			||||||
 | 
									required
 | 
				
			||||||
 | 
									bind:value={term}
 | 
				
			||||||
			/>
 | 
								/>
 | 
				
			||||||
		</div>
 | 
							</form>
 | 
				
			||||||
		<section class="flex gap-4 place-items-center">
 | 
							<section class="flex gap-4 place-items-center">
 | 
				
			||||||
			<ThemeButton />
 | 
								<ThemeButton />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										26
									
								
								web/src/routes/(user)/search/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								web/src/routes/(user)/search/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					import { redirect } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import type { PageServerLoad } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load = (async ({ locals, parent, url }) => {
 | 
				
			||||||
 | 
						const { user } = await parent();
 | 
				
			||||||
 | 
						if (!user) {
 | 
				
			||||||
 | 
							throw redirect(302, '/auth/login');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const term = url.searchParams.get('q') || undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { data: results } = await locals.api.searchApi.search(
 | 
				
			||||||
 | 
							term,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							undefined,
 | 
				
			||||||
 | 
							{ params: url.searchParams }
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
						return { user, term, results };
 | 
				
			||||||
 | 
					}) satisfies PageServerLoad;
 | 
				
			||||||
							
								
								
									
										27
									
								
								web/src/routes/(user)/search/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								web/src/routes/(user)/search/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { page } from '$app/stores';
 | 
				
			||||||
 | 
						import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
 | 
				
			||||||
 | 
						import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
 | 
				
			||||||
 | 
						import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let data: PageData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const term = $page.url.searchParams.get('q') || '';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section>
 | 
				
			||||||
 | 
						<NavigationBar {term} user={data.user} shouldShowUploadButton={false} />
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section class="relative pt-[72px] h-screen bg-immich-bg  dark:bg-immich-dark-bg">
 | 
				
			||||||
 | 
						<section class="overflow-y-auto relative immich-scrollbar">
 | 
				
			||||||
 | 
							<section
 | 
				
			||||||
 | 
								id="search-content"
 | 
				
			||||||
 | 
								class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
 | 
				
			||||||
 | 
							>
 | 
				
			||||||
 | 
								{#if data.results?.assets?.items}
 | 
				
			||||||
 | 
									<GalleryViewer assets={data.results.assets.items} />
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
 | 
							</section>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
@ -68,7 +68,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
					<div class="p-4 max-h-[75vh] min-h-[300px] overflow-y-auto immich-scrollbar pb-4 gap-4">
 | 
										<div class="p-4 max-h-[75vh] min-h-[300px] overflow-y-auto immich-scrollbar pb-4 gap-4">
 | 
				
			||||||
						<div class="flex flex-col w-full gap-2">
 | 
											<div class="flex flex-col w-full gap-2">
 | 
				
			||||||
							<p class="text-red-500">{$page.error?.message} - {$page.error?.code}</p>
 | 
												<p class="text-red-500">{$page.error?.message} ({$page.error?.code})</p>
 | 
				
			||||||
							{#if $page.error?.stack}
 | 
												{#if $page.error?.stack}
 | 
				
			||||||
								<label for="stacktrace">Stacktrace</label>
 | 
													<label for="stacktrace">Stacktrace</label>
 | 
				
			||||||
								<pre id="stacktrace" class="text-xs">{$page.error?.stack || 'No stack'}</pre>
 | 
													<pre id="stacktrace" class="text-xs">{$page.error?.stack || 'No stack'}</pre>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user