1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

fix(server): stacked assets for full sync, userIds as array for delta sync (#9100)

* fix(server): stacked assets for full sync, userIds as array for delta sync

* refactor(server): sync

* fix getDeltaSync after partner removal

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Fynn Petersen-Frey 2024-04-29 05:24:21 +02:00 committed by GitHub
parent fc2e709ad4
commit 32e7cfea3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 817 additions and 386 deletions

View file

@ -28,12 +28,14 @@ doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md doc/AssetBulkUploadCheckItem.md
doc/AssetBulkUploadCheckResponseDto.md doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md doc/AssetBulkUploadCheckResult.md
doc/AssetDeltaSyncDto.md
doc/AssetDeltaSyncResponseDto.md doc/AssetDeltaSyncResponseDto.md
doc/AssetFaceResponseDto.md doc/AssetFaceResponseDto.md
doc/AssetFaceUpdateDto.md doc/AssetFaceUpdateDto.md
doc/AssetFaceUpdateItem.md doc/AssetFaceUpdateItem.md
doc/AssetFaceWithoutPersonResponseDto.md doc/AssetFaceWithoutPersonResponseDto.md
doc/AssetFileUploadResponseDto.md doc/AssetFileUploadResponseDto.md
doc/AssetFullSyncDto.md
doc/AssetIdsDto.md doc/AssetIdsDto.md
doc/AssetIdsResponseDto.md doc/AssetIdsResponseDto.md
doc/AssetJobName.md doc/AssetJobName.md
@ -265,12 +267,14 @@ lib/model/asset_bulk_upload_check_dto.dart
lib/model/asset_bulk_upload_check_item.dart lib/model/asset_bulk_upload_check_item.dart
lib/model/asset_bulk_upload_check_response_dto.dart lib/model/asset_bulk_upload_check_response_dto.dart
lib/model/asset_bulk_upload_check_result.dart lib/model/asset_bulk_upload_check_result.dart
lib/model/asset_delta_sync_dto.dart
lib/model/asset_delta_sync_response_dto.dart lib/model/asset_delta_sync_response_dto.dart
lib/model/asset_face_response_dto.dart lib/model/asset_face_response_dto.dart
lib/model/asset_face_update_dto.dart lib/model/asset_face_update_dto.dart
lib/model/asset_face_update_item.dart lib/model/asset_face_update_item.dart
lib/model/asset_face_without_person_response_dto.dart lib/model/asset_face_without_person_response_dto.dart
lib/model/asset_file_upload_response_dto.dart lib/model/asset_file_upload_response_dto.dart
lib/model/asset_full_sync_dto.dart
lib/model/asset_ids_dto.dart lib/model/asset_ids_dto.dart
lib/model/asset_ids_response_dto.dart lib/model/asset_ids_response_dto.dart
lib/model/asset_job_name.dart lib/model/asset_job_name.dart
@ -449,12 +453,14 @@ test/asset_bulk_upload_check_dto_test.dart
test/asset_bulk_upload_check_item_test.dart test/asset_bulk_upload_check_item_test.dart
test/asset_bulk_upload_check_response_dto_test.dart test/asset_bulk_upload_check_response_dto_test.dart
test/asset_bulk_upload_check_result_test.dart test/asset_bulk_upload_check_result_test.dart
test/asset_delta_sync_dto_test.dart
test/asset_delta_sync_response_dto_test.dart test/asset_delta_sync_response_dto_test.dart
test/asset_face_response_dto_test.dart test/asset_face_response_dto_test.dart
test/asset_face_update_dto_test.dart test/asset_face_update_dto_test.dart
test/asset_face_update_item_test.dart test/asset_face_update_item_test.dart
test/asset_face_without_person_response_dto_test.dart test/asset_face_without_person_response_dto_test.dart
test/asset_file_upload_response_dto_test.dart test/asset_file_upload_response_dto_test.dart
test/asset_full_sync_dto_test.dart
test/asset_ids_dto_test.dart test/asset_ids_dto_test.dart
test/asset_ids_response_dto_test.dart test/asset_ids_response_dto_test.dart
test/asset_job_name_test.dart test/asset_job_name_test.dart

View file

@ -186,8 +186,8 @@ Class | Method | HTTP request | Description
*SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} | *SharedLinkApi* | [**removeSharedLink**](doc//SharedLinkApi.md#removesharedlink) | **DELETE** /shared-link/{id} |
*SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets | *SharedLinkApi* | [**removeSharedLinkAssets**](doc//SharedLinkApi.md#removesharedlinkassets) | **DELETE** /shared-link/{id}/assets |
*SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} | *SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} |
*SyncApi* | [**getAllForUserFullSync**](doc//SyncApi.md#getallforuserfullsync) | **GET** /sync/full-sync | *SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync |
*SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **GET** /sync/delta-sync | *SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync |
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
*SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults | *SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults |
*SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json | *SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json |
@ -244,12 +244,14 @@ Class | Method | HTTP request | Description
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md) - [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md) - [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
- [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md)
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md) - [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md) - [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
- [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md)
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md) - [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
- [AssetFullSyncDto](doc//AssetFullSyncDto.md)
- [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsDto](doc//AssetIdsDto.md)
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
- [AssetJobName](doc//AssetJobName.md) - [AssetJobName](doc//AssetJobName.md)

16
mobile/openapi/doc/AssetDeltaSyncDto.md generated Normal file
View file

@ -0,0 +1,16 @@
# openapi.model.AssetDeltaSyncDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**updatedAfter** | [**DateTime**](DateTime.md) | |
**userIds** | **List<String>** | | [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)

19
mobile/openapi/doc/AssetFullSyncDto.md generated Normal file
View file

@ -0,0 +1,19 @@
# openapi.model.AssetFullSyncDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**lastCreationDate** | [**DateTime**](DateTime.md) | | [optional]
**lastId** | **String** | | [optional]
**limit** | **int** | |
**updatedUntil** | [**DateTime**](DateTime.md) | |
**userId** | **String** | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -9,75 +9,12 @@ All URIs are relative to */api*
Method | HTTP request | Description Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**getAllForUserFullSync**](SyncApi.md#getallforuserfullsync) | **GET** /sync/full-sync | [**getDeltaSync**](SyncApi.md#getdeltasync) | **POST** /sync/delta-sync |
[**getDeltaSync**](SyncApi.md#getdeltasync) | **GET** /sync/delta-sync | [**getFullSyncForUser**](SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync |
# **getAllForUserFullSync**
> List<AssetResponseDto> getAllForUserFullSync(limit, updatedUntil, lastCreationDate, lastId, userId)
### Example
```dart
import 'package:openapi/api.dart';
// 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';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// 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);
final api_instance = SyncApi();
final limit = 56; // int |
final updatedUntil = 2013-10-20T19:20:30+01:00; // DateTime |
final lastCreationDate = 2013-10-20T19:20:30+01:00; // DateTime |
final lastId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
final result = api_instance.getAllForUserFullSync(limit, updatedUntil, lastCreationDate, lastId, userId);
print(result);
} catch (e) {
print('Exception when calling SyncApi->getAllForUserFullSync: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**limit** | **int**| |
**updatedUntil** | **DateTime**| |
**lastCreationDate** | **DateTime**| | [optional]
**lastId** | **String**| | [optional]
**userId** | **String**| | [optional]
### Return type
[**List<AssetResponseDto>**](AssetResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### 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)
# **getDeltaSync** # **getDeltaSync**
> AssetDeltaSyncResponseDto getDeltaSync(updatedAfter, userIds) > AssetDeltaSyncResponseDto getDeltaSync(assetDeltaSyncDto)
@ -100,11 +37,10 @@ import 'package:openapi/api.dart';
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SyncApi(); final api_instance = SyncApi();
final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final assetDeltaSyncDto = AssetDeltaSyncDto(); // AssetDeltaSyncDto |
final userIds = []; // List<String> |
try { try {
final result = api_instance.getDeltaSync(updatedAfter, userIds); final result = api_instance.getDeltaSync(assetDeltaSyncDto);
print(result); print(result);
} catch (e) { } catch (e) {
print('Exception when calling SyncApi->getDeltaSync: $e\n'); print('Exception when calling SyncApi->getDeltaSync: $e\n');
@ -115,8 +51,7 @@ try {
Name | Type | Description | Notes Name | Type | Description | Notes
------------- | ------------- | ------------- | ------------- ------------- | ------------- | ------------- | -------------
**updatedAfter** | **DateTime**| | **assetDeltaSyncDto** | [**AssetDeltaSyncDto**](AssetDeltaSyncDto.md)| |
**userIds** | [**List<String>**](String.md)| | [default to const []]
### Return type ### Return type
@ -128,7 +63,62 @@ Name | Type | Description | Notes
### HTTP request headers ### HTTP request headers
- **Content-Type**: Not defined - **Content-Type**: application/json
- **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)
# **getFullSyncForUser**
> List<AssetResponseDto> getFullSyncForUser(assetFullSyncDto)
### Example
```dart
import 'package:openapi/api.dart';
// 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';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// 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);
final api_instance = SyncApi();
final assetFullSyncDto = AssetFullSyncDto(); // AssetFullSyncDto |
try {
final result = api_instance.getFullSyncForUser(assetFullSyncDto);
print(result);
} catch (e) {
print('Exception when calling SyncApi->getFullSyncForUser: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**assetFullSyncDto** | [**AssetFullSyncDto**](AssetFullSyncDto.md)| |
### Return type
[**List<AssetResponseDto>**](AssetResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json - **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) [[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)

View file

@ -77,12 +77,14 @@ part 'model/asset_bulk_upload_check_dto.dart';
part 'model/asset_bulk_upload_check_item.dart'; part 'model/asset_bulk_upload_check_item.dart';
part 'model/asset_bulk_upload_check_response_dto.dart'; part 'model/asset_bulk_upload_check_response_dto.dart';
part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_bulk_upload_check_result.dart';
part 'model/asset_delta_sync_dto.dart';
part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_delta_sync_response_dto.dart';
part 'model/asset_face_response_dto.dart'; part 'model/asset_face_response_dto.dart';
part 'model/asset_face_update_dto.dart'; part 'model/asset_face_update_dto.dart';
part 'model/asset_face_update_item.dart'; part 'model/asset_face_update_item.dart';
part 'model/asset_face_without_person_response_dto.dart'; part 'model/asset_face_without_person_response_dto.dart';
part 'model/asset_file_upload_response_dto.dart'; part 'model/asset_file_upload_response_dto.dart';
part 'model/asset_full_sync_dto.dart';
part 'model/asset_ids_dto.dart'; part 'model/asset_ids_dto.dart';
part 'model/asset_ids_response_dto.dart'; part 'model/asset_ids_response_dto.dart';
part 'model/asset_job_name.dart'; part 'model/asset_job_name.dart';

View file

@ -16,47 +16,27 @@ class SyncApi {
final ApiClient apiClient; final ApiClient apiClient;
/// Performs an HTTP 'GET /sync/full-sync' operation and returns the [Response]. /// Performs an HTTP 'POST /sync/delta-sync' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [int] limit (required): /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required):
/// Future<Response> getDeltaSyncWithHttpInfo(AssetDeltaSyncDto assetDeltaSyncDto,) async {
/// * [DateTime] updatedUntil (required):
///
/// * [DateTime] lastCreationDate:
///
/// * [String] lastId:
///
/// * [String] userId:
Future<Response> getAllForUserFullSyncWithHttpInfo(int limit, DateTime updatedUntil, { DateTime? lastCreationDate, String? lastId, String? userId, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/sync/full-sync'; final path = r'/sync/delta-sync';
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody; Object? postBody = assetDeltaSyncDto;
final queryParams = <QueryParam>[]; final queryParams = <QueryParam>[];
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <String, String>{}; final formParams = <String, String>{};
if (lastCreationDate != null) { const contentTypes = <String>['application/json'];
queryParams.addAll(_queryParams('', 'lastCreationDate', lastCreationDate));
}
if (lastId != null) {
queryParams.addAll(_queryParams('', 'lastId', lastId));
}
queryParams.addAll(_queryParams('', 'limit', limit));
queryParams.addAll(_queryParams('', 'updatedUntil', updatedUntil));
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
const contentTypes = <String>[];
return apiClient.invokeAPI( return apiClient.invokeAPI(
path, path,
'GET', 'POST',
queryParams, queryParams,
postBody, postBody,
headerParams, headerParams,
@ -67,17 +47,56 @@ class SyncApi {
/// Parameters: /// Parameters:
/// ///
/// * [int] limit (required): /// * [AssetDeltaSyncDto] assetDeltaSyncDto (required):
Future<AssetDeltaSyncResponseDto?> getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto,) async {
final response = await getDeltaSyncWithHttpInfo(assetDeltaSyncDto,);
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), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /sync/full-sync' operation and returns the [Response].
/// Parameters:
/// ///
/// * [DateTime] updatedUntil (required): /// * [AssetFullSyncDto] assetFullSyncDto (required):
Future<Response> getFullSyncForUserWithHttpInfo(AssetFullSyncDto assetFullSyncDto,) async {
// ignore: prefer_const_declarations
final path = r'/sync/full-sync';
// ignore: prefer_final_locals
Object? postBody = assetFullSyncDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
/// ///
/// * [DateTime] lastCreationDate: /// * [AssetFullSyncDto] assetFullSyncDto (required):
/// Future<List<AssetResponseDto>?> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto,) async {
/// * [String] lastId: final response = await getFullSyncForUserWithHttpInfo(assetFullSyncDto,);
///
/// * [String] userId:
Future<List<AssetResponseDto>?> getAllForUserFullSync(int limit, DateTime updatedUntil, { DateTime? lastCreationDate, String? lastId, String? userId, }) async {
final response = await getAllForUserFullSyncWithHttpInfo(limit, updatedUntil, lastCreationDate: lastCreationDate, lastId: lastId, userId: userId, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@ -93,58 +112,4 @@ class SyncApi {
} }
return null; return null;
} }
/// Performs an HTTP 'GET /sync/delta-sync' operation and returns the [Response].
/// Parameters:
///
/// * [DateTime] updatedAfter (required):
///
/// * [List<String>] userIds (required):
Future<Response> getDeltaSyncWithHttpInfo(DateTime updatedAfter, List<String> userIds,) async {
// ignore: prefer_const_declarations
final path = r'/sync/delta-sync';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter));
queryParams.addAll(_queryParams('multi', 'userIds', userIds));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [DateTime] updatedAfter (required):
///
/// * [List<String>] userIds (required):
Future<AssetDeltaSyncResponseDto?> getDeltaSync(DateTime updatedAfter, List<String> userIds,) async {
final response = await getDeltaSyncWithHttpInfo(updatedAfter, userIds,);
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), 'AssetDeltaSyncResponseDto',) as AssetDeltaSyncResponseDto;
}
return null;
}
} }

View file

@ -224,6 +224,8 @@ class ApiClient {
return AssetBulkUploadCheckResponseDto.fromJson(value); return AssetBulkUploadCheckResponseDto.fromJson(value);
case 'AssetBulkUploadCheckResult': case 'AssetBulkUploadCheckResult':
return AssetBulkUploadCheckResult.fromJson(value); return AssetBulkUploadCheckResult.fromJson(value);
case 'AssetDeltaSyncDto':
return AssetDeltaSyncDto.fromJson(value);
case 'AssetDeltaSyncResponseDto': case 'AssetDeltaSyncResponseDto':
return AssetDeltaSyncResponseDto.fromJson(value); return AssetDeltaSyncResponseDto.fromJson(value);
case 'AssetFaceResponseDto': case 'AssetFaceResponseDto':
@ -236,6 +238,8 @@ class ApiClient {
return AssetFaceWithoutPersonResponseDto.fromJson(value); return AssetFaceWithoutPersonResponseDto.fromJson(value);
case 'AssetFileUploadResponseDto': case 'AssetFileUploadResponseDto':
return AssetFileUploadResponseDto.fromJson(value); return AssetFileUploadResponseDto.fromJson(value);
case 'AssetFullSyncDto':
return AssetFullSyncDto.fromJson(value);
case 'AssetIdsDto': case 'AssetIdsDto':
return AssetIdsDto.fromJson(value); return AssetIdsDto.fromJson(value);
case 'AssetIdsResponseDto': case 'AssetIdsResponseDto':

View file

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

View file

@ -0,0 +1,158 @@
//
// 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 AssetFullSyncDto {
/// Returns a new [AssetFullSyncDto] instance.
AssetFullSyncDto({
this.lastCreationDate,
this.lastId,
required this.limit,
required this.updatedUntil,
this.userId,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? lastCreationDate;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? lastId;
/// Minimum value: 1
int limit;
DateTime updatedUntil;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? userId;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto &&
other.lastCreationDate == lastCreationDate &&
other.lastId == lastId &&
other.limit == limit &&
other.updatedUntil == updatedUntil &&
other.userId == userId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(lastCreationDate == null ? 0 : lastCreationDate!.hashCode) +
(lastId == null ? 0 : lastId!.hashCode) +
(limit.hashCode) +
(updatedUntil.hashCode) +
(userId == null ? 0 : userId!.hashCode);
@override
String toString() => 'AssetFullSyncDto[lastCreationDate=$lastCreationDate, lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.lastCreationDate != null) {
json[r'lastCreationDate'] = this.lastCreationDate!.toUtc().toIso8601String();
} else {
// json[r'lastCreationDate'] = null;
}
if (this.lastId != null) {
json[r'lastId'] = this.lastId;
} else {
// json[r'lastId'] = null;
}
json[r'limit'] = this.limit;
json[r'updatedUntil'] = this.updatedUntil.toUtc().toIso8601String();
if (this.userId != null) {
json[r'userId'] = this.userId;
} else {
// json[r'userId'] = null;
}
return json;
}
/// Returns a new [AssetFullSyncDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetFullSyncDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return AssetFullSyncDto(
lastCreationDate: mapDateTime(json, r'lastCreationDate', r''),
lastId: mapValueOfType<String>(json, r'lastId'),
limit: mapValueOfType<int>(json, r'limit')!,
updatedUntil: mapDateTime(json, r'updatedUntil', r'')!,
userId: mapValueOfType<String>(json, r'userId'),
);
}
return null;
}
static List<AssetFullSyncDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetFullSyncDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetFullSyncDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetFullSyncDto> mapFromJson(dynamic json) {
final map = <String, AssetFullSyncDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetFullSyncDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetFullSyncDto-objects as value to a dart map
static Map<String, List<AssetFullSyncDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetFullSyncDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetFullSyncDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'limit',
'updatedUntil',
};
}

View 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 AssetDeltaSyncDto
void main() {
// final instance = AssetDeltaSyncDto();
group('test AssetDeltaSyncDto', () {
// DateTime updatedAfter
test('to test the property `updatedAfter`', () async {
// TODO
});
// List<String> userIds (default value: const [])
test('to test the property `userIds`', () async {
// TODO
});
});
}

View file

@ -0,0 +1,47 @@
//
// 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 AssetFullSyncDto
void main() {
// final instance = AssetFullSyncDto();
group('test AssetFullSyncDto', () {
// DateTime lastCreationDate
test('to test the property `lastCreationDate`', () async {
// TODO
});
// String lastId
test('to test the property `lastId`', () async {
// TODO
});
// int limit
test('to test the property `limit`', () async {
// TODO
});
// DateTime updatedUntil
test('to test the property `updatedUntil`', () async {
// TODO
});
// String userId
test('to test the property `userId`', () async {
// TODO
});
});
}

View file

@ -17,13 +17,13 @@ void main() {
// final instance = SyncApi(); // final instance = SyncApi();
group('tests for SyncApi', () { group('tests for SyncApi', () {
//Future<List<AssetResponseDto>> getAllForUserFullSync(int limit, DateTime updatedUntil, { DateTime lastCreationDate, String lastId, String userId }) async //Future<AssetDeltaSyncResponseDto> getDeltaSync(AssetDeltaSyncDto assetDeltaSyncDto) async
test('test getAllForUserFullSync', () async { test('test getDeltaSync', () async {
// TODO // TODO
}); });
//Future<AssetDeltaSyncResponseDto> getDeltaSync(DateTime updatedAfter, List<String> userIds) async //Future<List<AssetResponseDto>> getFullSyncForUser(AssetFullSyncDto assetFullSyncDto) async
test('test getDeltaSync', () async { test('test getFullSyncForUser', () async {
// TODO // TODO
}); });

View file

@ -4958,31 +4958,19 @@
} }
}, },
"/sync/delta-sync": { "/sync/delta-sync": {
"get": { "post": {
"operationId": "getDeltaSync", "operationId": "getDeltaSync",
"parameters": [ "parameters": [],
{ "requestBody": {
"name": "updatedAfter", "content": {
"required": true, "application/json": {
"in": "query", "schema": {
"schema": { "$ref": "#/components/schemas/AssetDeltaSyncDto"
"format": "date-time",
"type": "string"
}
},
{
"name": "userIds",
"required": true,
"in": "query",
"schema": {
"format": "uuid",
"type": "array",
"items": {
"type": "string"
} }
} }
} },
], "required": true
},
"responses": { "responses": {
"200": { "200": {
"content": { "content": {
@ -5012,55 +5000,19 @@
} }
}, },
"/sync/full-sync": { "/sync/full-sync": {
"get": { "post": {
"operationId": "getAllForUserFullSync", "operationId": "getFullSyncForUser",
"parameters": [ "parameters": [],
{ "requestBody": {
"name": "lastCreationDate", "content": {
"required": false, "application/json": {
"in": "query", "schema": {
"schema": { "$ref": "#/components/schemas/AssetFullSyncDto"
"format": "date-time", }
"type": "string"
} }
}, },
{ "required": true
"name": "lastId", },
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "limit",
"required": true,
"in": "query",
"schema": {
"minimum": 1,
"type": "integer"
}
},
{
"name": "updatedUntil",
"required": true,
"in": "query",
"schema": {
"format": "date-time",
"type": "string"
}
},
{
"name": "userId",
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": { "responses": {
"200": { "200": {
"content": { "content": {
@ -7023,6 +6975,26 @@
], ],
"type": "object" "type": "object"
}, },
"AssetDeltaSyncDto": {
"properties": {
"updatedAfter": {
"format": "date-time",
"type": "string"
},
"userIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
}
},
"required": [
"updatedAfter",
"userIds"
],
"type": "object"
},
"AssetDeltaSyncResponseDto": { "AssetDeltaSyncResponseDto": {
"properties": { "properties": {
"deleted": { "deleted": {
@ -7175,6 +7147,35 @@
], ],
"type": "object" "type": "object"
}, },
"AssetFullSyncDto": {
"properties": {
"lastCreationDate": {
"format": "date-time",
"type": "string"
},
"lastId": {
"format": "uuid",
"type": "string"
},
"limit": {
"minimum": 1,
"type": "integer"
},
"updatedUntil": {
"format": "date-time",
"type": "string"
},
"userId": {
"format": "uuid",
"type": "string"
}
},
"required": [
"limit",
"updatedUntil"
],
"type": "object"
},
"AssetIdsDto": { "AssetIdsDto": {
"properties": { "properties": {
"assetIds": { "assetIds": {

View file

@ -836,11 +836,22 @@ export type AssetIdsResponseDto = {
error?: Error2; error?: Error2;
success: boolean; success: boolean;
}; };
export type AssetDeltaSyncDto = {
updatedAfter: string;
userIds: string[];
};
export type AssetDeltaSyncResponseDto = { export type AssetDeltaSyncResponseDto = {
deleted: string[]; deleted: string[];
needsFullSync: boolean; needsFullSync: boolean;
upserted: AssetResponseDto[]; upserted: AssetResponseDto[];
}; };
export type AssetFullSyncDto = {
lastCreationDate?: string;
lastId?: string;
limit: number;
updatedUntil: string;
userId?: string;
};
export type SystemConfigFFmpegDto = { export type SystemConfigFFmpegDto = {
accel: TranscodeHWAccel; accel: TranscodeHWAccel;
acceptedAudioCodecs: AudioCodec[]; acceptedAudioCodecs: AudioCodec[];
@ -2372,39 +2383,29 @@ export function addSharedLinkAssets({ id, key, assetIdsDto }: {
body: assetIdsDto body: assetIdsDto
}))); })));
} }
export function getDeltaSync({ updatedAfter, userIds }: { export function getDeltaSync({ assetDeltaSyncDto }: {
updatedAfter: string; assetDeltaSyncDto: AssetDeltaSyncDto;
userIds: string[];
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: AssetDeltaSyncResponseDto; data: AssetDeltaSyncResponseDto;
}>(`/sync/delta-sync${QS.query(QS.explode({ }>("/sync/delta-sync", oazapfts.json({
updatedAfter, ...opts,
userIds method: "POST",
}))}`, { body: assetDeltaSyncDto
...opts })));
}));
} }
export function getAllForUserFullSync({ lastCreationDate, lastId, limit, updatedUntil, userId }: { export function getFullSyncForUser({ assetFullSyncDto }: {
lastCreationDate?: string; assetFullSyncDto: AssetFullSyncDto;
lastId?: string;
limit: number;
updatedUntil: string;
userId?: string;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: AssetResponseDto[]; data: AssetResponseDto[];
}>(`/sync/full-sync${QS.query(QS.explode({ }>("/sync/full-sync", oazapfts.json({
lastCreationDate, ...opts,
lastId, method: "POST",
limit, body: assetFullSyncDto
updatedUntil, })));
userId
}))}`, {
...opts
}));
} }
export function getConfig(opts?: Oazapfts.RequestOpts) { export function getConfig(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{

View file

@ -1,4 +1,4 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@ -12,13 +12,15 @@ import { SyncService } from 'src/services/sync.service';
export class SyncController { export class SyncController {
constructor(private service: SyncService) {} constructor(private service: SyncService) {}
@Get('full-sync') @Post('full-sync')
getAllForUserFullSync(@Auth() auth: AuthDto, @Query() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> { @HttpCode(HttpStatus.OK)
return this.service.getAllAssetsForUserFullSync(auth, dto); getFullSyncForUser(@Auth() auth: AuthDto, @Body() dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
return this.service.getFullSync(auth, dto);
} }
@Get('delta-sync') @Post('delta-sync')
getDeltaSync(@Auth() auth: AuthDto, @Query() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> { @HttpCode(HttpStatus.OK)
return this.service.getChangesForDeltaSync(auth, dto); getDeltaSync(@Auth() auth: AuthDto, @Body() dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
return this.service.getDeltaSync(auth, dto);
} }
} }

View file

@ -1,5 +1,4 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, IsPositive } from 'class-validator'; import { IsInt, IsPositive } from 'class-validator';
import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { ValidateDate, ValidateUUID } from 'src/validation'; import { ValidateDate, ValidateUUID } from 'src/validation';
@ -16,7 +15,6 @@ export class AssetFullSyncDto {
@IsInt() @IsInt()
@IsPositive() @IsPositive()
@Type(() => Number)
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
limit!: number; limit!: number;
@ -27,6 +25,7 @@ export class AssetFullSyncDto {
export class AssetDeltaSyncDto { export class AssetDeltaSyncDto {
@ValidateDate() @ValidateDate()
updatedAfter!: Date; updatedAfter!: Date;
@ValidateUUID({ each: true }) @ValidateUUID({ each: true })
userIds!: string[]; userIds!: string[];
} }

View file

@ -134,6 +134,8 @@ export interface AssetFullSyncOptions {
lastCreationDate?: Date; lastCreationDate?: Date;
lastId?: string; lastId?: string;
updatedUntil: Date; updatedUntil: Date;
isArchived?: false;
withStacked?: true;
limit: number; limit: number;
} }

View file

@ -798,16 +798,47 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps", "exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id", "stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId" "stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId"
FROM FROM
"assets" "asset" "assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE WHERE
"asset"."ownerId" = $1 "asset"."isVisible" = true
AND "asset"."ownerId" IN ($1)
AND ("asset"."fileCreatedAt", "asset"."id") < ($2, $3) AND ("asset"."fileCreatedAt", "asset"."id") < ($2, $3)
AND "asset"."updatedAt" <= $4 AND "asset"."updatedAt" <= $4
AND "asset"."isVisible" = true
ORDER BY ORDER BY
"asset"."fileCreatedAt" DESC, "asset"."fileCreatedAt" DESC,
"asset"."id" DESC "asset"."id" DESC
@ -816,72 +847,105 @@ LIMIT
-- AssetRepository.getChangedDeltaSync -- AssetRepository.getChangedDeltaSync
SELECT SELECT
"AssetEntity"."id" AS "AssetEntity_id", "asset"."id" AS "asset_id",
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", "asset"."deviceAssetId" AS "asset_deviceAssetId",
"AssetEntity"."ownerId" AS "AssetEntity_ownerId", "asset"."ownerId" AS "asset_ownerId",
"AssetEntity"."libraryId" AS "AssetEntity_libraryId", "asset"."libraryId" AS "asset_libraryId",
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "asset"."deviceId" AS "asset_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "asset"."type" AS "asset_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "asset"."originalPath" AS "asset_originalPath",
"AssetEntity"."previewPath" AS "AssetEntity_previewPath", "asset"."previewPath" AS "asset_previewPath",
"AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "asset"."thumbnailPath" AS "asset_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "asset"."createdAt" AS "asset_createdAt",
"AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", "asset"."updatedAt" AS "asset_updatedAt",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", "asset"."deletedAt" AS "asset_deletedAt",
"AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", "asset"."fileCreatedAt" AS "asset_fileCreatedAt",
"AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", "asset"."localDateTime" AS "asset_localDateTime",
"AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", "asset"."fileModifiedAt" AS "asset_fileModifiedAt",
"AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", "asset"."isFavorite" AS "asset_isFavorite",
"AssetEntity"."isArchived" AS "AssetEntity_isArchived", "asset"."isArchived" AS "asset_isArchived",
"AssetEntity"."isExternal" AS "AssetEntity_isExternal", "asset"."isExternal" AS "asset_isExternal",
"AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", "asset"."isReadOnly" AS "asset_isReadOnly",
"AssetEntity"."isOffline" AS "AssetEntity_isOffline", "asset"."isOffline" AS "asset_isOffline",
"AssetEntity"."checksum" AS "AssetEntity_checksum", "asset"."checksum" AS "asset_checksum",
"AssetEntity"."duration" AS "AssetEntity_duration", "asset"."duration" AS "asset_duration",
"AssetEntity"."isVisible" AS "AssetEntity_isVisible", "asset"."isVisible" AS "asset_isVisible",
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", "asset"."originalFileName" AS "asset_originalFileName",
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", "asset"."sidecarPath" AS "asset_sidecarPath",
"AssetEntity"."stackId" AS "AssetEntity_stackId", "asset"."stackId" AS "asset_stackId",
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId", "exifInfo"."assetId" AS "exifInfo_assetId",
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description", "exifInfo"."description" AS "exifInfo_description",
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
"AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight", "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
"AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte", "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
"AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation", "exifInfo"."orientation" AS "exifInfo_orientation",
"AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal", "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
"AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate", "exifInfo"."modifyDate" AS "exifInfo_modifyDate",
"AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone", "exifInfo"."timeZone" AS "exifInfo_timeZone",
"AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude", "exifInfo"."latitude" AS "exifInfo_latitude",
"AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude", "exifInfo"."longitude" AS "exifInfo_longitude",
"AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType", "exifInfo"."projectionType" AS "exifInfo_projectionType",
"AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city", "exifInfo"."city" AS "exifInfo_city",
"AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID", "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
"AssetEntity__AssetEntity_exifInfo"."autoStackId" AS "AssetEntity__AssetEntity_exifInfo_autoStackId", "exifInfo"."autoStackId" AS "exifInfo_autoStackId",
"AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state", "exifInfo"."state" AS "exifInfo_state",
"AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country", "exifInfo"."country" AS "exifInfo_country",
"AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make", "exifInfo"."make" AS "exifInfo_make",
"AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model", "exifInfo"."model" AS "exifInfo_model",
"AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel", "exifInfo"."lensModel" AS "exifInfo_lensModel",
"AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber", "exifInfo"."fNumber" AS "exifInfo_fNumber",
"AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength", "exifInfo"."focalLength" AS "exifInfo_focalLength",
"AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso", "exifInfo"."iso" AS "exifInfo_iso",
"AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime", "exifInfo"."exposureTime" AS "exifInfo_exposureTime",
"AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", "exifInfo"."profileDescription" AS "exifInfo_profileDescription",
"AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", "exifInfo"."colorspace" AS "exifInfo_colorspace",
"AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps", "exifInfo"."fps" AS "exifInfo_fps",
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id", "stack"."id" AS "stack_id",
"AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId" "stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isReadOnly" AS "stackedAssets_isReadOnly",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId"
FROM FROM
"assets" "AssetEntity" "assets" "asset"
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE WHERE
( "asset"."isVisible" = true
("AssetEntity"."ownerId" IN ($1)) AND "asset"."ownerId" IN ($1)
AND ("AssetEntity"."isVisible" = $2) AND (
AND ("AssetEntity"."updatedAt" > $3) "stack"."primaryAssetId" = "asset"."id"
OR "asset"."stackId" IS NULL
) )
AND "asset"."updatedAt" > $2

View file

@ -710,21 +710,23 @@ export class AssetRepository implements IAssetRepository {
], ],
}) })
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> { getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
const { ownerId, lastCreationDate, lastId, updatedUntil, limit } = options; const { ownerId, isArchived, withStacked, lastCreationDate, lastId, updatedUntil, limit } = options;
const builder = this.repository const builder = this.getBuilder({
.createQueryBuilder('asset') userIds: [ownerId],
.leftJoinAndSelect('asset.exifInfo', 'exifInfo') exifInfo: true,
.leftJoinAndSelect('asset.stack', 'stack') withStacked,
.where('asset.ownerId = :ownerId', { ownerId }); isArchived,
});
if (lastCreationDate !== undefined && lastId !== undefined) { if (lastCreationDate !== undefined && lastId !== undefined) {
builder.andWhere('(asset.fileCreatedAt, asset.id) < (:lastCreationDate, :lastId)', { builder.andWhere('(asset.fileCreatedAt, asset.id) < (:lastCreationDate, :lastId)', {
lastCreationDate, lastCreationDate,
lastId, lastId,
}); });
} }
return builder return builder
.andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil }) .andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil })
.andWhere('asset.isVisible = true')
.orderBy('asset.fileCreatedAt', 'DESC') .orderBy('asset.fileCreatedAt', 'DESC')
.addOrderBy('asset.id', 'DESC') .addOrderBy('asset.id', 'DESC')
.limit(limit) .limit(limit)
@ -734,18 +736,11 @@ export class AssetRepository implements IAssetRepository {
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] }) @GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] })
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> { getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
return this.repository.find({ const builder = this.getBuilder({ userIds: options.userIds, exifInfo: true, withStacked: true })
where: { .andWhere({ updatedAt: MoreThan(options.updatedAfter) })
ownerId: In(options.userIds), .take(options.limit)
isVisible: true, .withDeleted();
updatedAt: MoreThan(options.updatedAfter),
}, return builder.getMany();
relations: {
exifInfo: true,
stack: true,
},
take: options.limit,
withDeleted: true,
});
} }
} }

View file

@ -39,13 +39,12 @@ describe(SyncService.name, () => {
describe('getAllAssetsForUserFullSync', () => { describe('getAllAssetsForUserFullSync', () => {
it('should return a list of all assets owned by the user', async () => { it('should return a list of all assets owned by the user', async () => {
assetMock.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]); assetMock.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]);
await expect( await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([
sut.getAllAssetsForUserFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate }),
).resolves.toEqual([
mapAsset(assetStub.external, mapAssetOpts), mapAsset(assetStub.external, mapAssetOpts),
mapAsset(assetStub.hasEncodedVideo, mapAssetOpts), mapAsset(assetStub.hasEncodedVideo, mapAssetOpts),
]); ]);
expect(assetMock.getAllForUserFullSync).toHaveBeenCalledWith({ expect(assetMock.getAllForUserFullSync).toHaveBeenCalledWith({
withStacked: true,
ownerId: authStub.user1.user.id, ownerId: authStub.user1.user.id,
updatedUntil: untilDate, updatedUntil: untilDate,
limit: 2, limit: 2,
@ -57,7 +56,7 @@ describe(SyncService.name, () => {
it('should return a response requiring a full sync when partners are out of sync', async () => { it('should return a response requiring a full sync when partners are out of sync', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1]);
await expect( await expect(
sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0);
expect(auditMock.getAfter).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
@ -66,7 +65,7 @@ describe(SyncService.name, () => {
it('should return a response requiring a full sync when last sync was too long ago', async () => { it('should return a response requiring a full sync when last sync was too long ago', async () => {
partnerMock.getAll.mockResolvedValue([]); partnerMock.getAll.mockResolvedValue([]);
await expect( await expect(
sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }), sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(2000), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(0);
expect(auditMock.getAfter).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
@ -78,7 +77,7 @@ describe(SyncService.name, () => {
Array.from<AssetEntity>({ length: 10_000 }).fill(assetStub.image), Array.from<AssetEntity>({ length: 10_000 }).fill(assetStub.image),
); );
await expect( await expect(
sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] });
expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(1); expect(assetMock.getChangedDeltaSync).toHaveBeenCalledTimes(1);
expect(auditMock.getAfter).toHaveBeenCalledTimes(0); expect(auditMock.getAfter).toHaveBeenCalledTimes(0);
@ -89,7 +88,7 @@ describe(SyncService.name, () => {
assetMock.getChangedDeltaSync.mockResolvedValue([assetStub.image1]); assetMock.getChangedDeltaSync.mockResolvedValue([assetStub.image1]);
auditMock.getAfter.mockResolvedValue([assetStub.external.id]); auditMock.getAfter.mockResolvedValue([assetStub.external.id]);
await expect( await expect(
sut.getChangesForDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }),
).resolves.toEqual({ ).resolves.toEqual({
needsFullSync: false, needsFullSync: false,
upserted: [mapAsset(assetStub.image1, mapAssetOpts)], upserted: [mapAsset(assetStub.image1, mapAssetOpts)],

View file

@ -1,5 +1,4 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import _ from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core'; import { AccessCore, Permission } from 'src/cores/access.core';
@ -11,6 +10,9 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { setIsEqual } from 'src/utils/set';
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
export class SyncService { export class SyncService {
private access: AccessCore; private access: AccessCore;
@ -24,52 +26,69 @@ export class SyncService {
this.access = AccessCore.create(accessRepository); this.access = AccessCore.create(accessRepository);
} }
async getAllAssetsForUserFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> { async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
// mobile implementation is faster if this is a single id
const userId = dto.userId || auth.user.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this.assetRepository.getAllForUserFullSync({ const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId, ownerId: userId,
// no archived assets for partner user
isArchived: userId === auth.user.id ? undefined : false,
// no stack for partner user
withStacked: userId === auth.user.id ? true : undefined,
lastCreationDate: dto.lastCreationDate, lastCreationDate: dto.lastCreationDate,
updatedUntil: dto.updatedUntil, updatedUntil: dto.updatedUntil,
lastId: dto.lastId, lastId: dto.lastId,
limit: dto.limit, limit: dto.limit,
}); });
const options = { auth, stripMetadata: false, withStack: true }; return assets.map((a) => mapAsset(a, { auth, stripMetadata: false, withStack: true }));
return assets.map((a) => mapAsset(a, options));
} }
async getChangesForDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> { async getDeltaSync(auth: AuthDto, dto: AssetDeltaSyncDto): Promise<AssetDeltaSyncResponseDto> {
await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); // app has not synced in the last 100 days
const partner = await this.partnerRepository.getAll(auth.user.id);
const userIds = [auth.user.id, ...partner.filter((p) => p.sharedWithId == auth.user.id).map((p) => p.sharedById)];
userIds.sort();
dto.userIds.sort();
const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter)); const duration = DateTime.now().diff(DateTime.fromJSDate(dto.updatedAfter));
if (duration > AUDIT_LOG_MAX_DURATION) {
if (!_.isEqual(userIds, dto.userIds) || duration > AUDIT_LOG_MAX_DURATION) { return FULL_SYNC;
// app does not have the correct partners synced
// or app has not synced in the last 100 days
return { needsFullSync: true, deleted: [], upserted: [] };
} }
const authUserId = auth.user.id;
// app does not have the correct partners synced
const partner = await this.partnerRepository.getAll(authUserId);
const userIds = [authUserId, ...partner.filter((p) => p.sharedWithId == auth.user.id).map((p) => p.sharedById)];
if (!setIsEqual(new Set(userIds), new Set(dto.userIds))) {
return FULL_SYNC;
}
await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds);
const limit = 10_000; const limit = 10_000;
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });
// too many changes, need to do a full sync
if (upserted.length === limit) { if (upserted.length === limit) {
// too many changes -> do a full sync (paginated) instead return FULL_SYNC;
return { needsFullSync: true, deleted: [], upserted: [] };
} }
const deleted = await this.auditRepository.getAfter(dto.updatedAfter, { const deleted = await this.auditRepository.getAfter(dto.updatedAfter, {
userIds: userIds, userIds,
entityType: EntityType.ASSET, entityType: EntityType.ASSET,
action: DatabaseAction.DELETE, action: DatabaseAction.DELETE,
}); });
const options = { auth, stripMetadata: false, withStack: true };
const result = { const result = {
needsFullSync: false, needsFullSync: false,
upserted: upserted.map((a) => mapAsset(a, options)), upserted: upserted
// do not return archived assets for partner users
.filter((a) => a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && !a.isArchived))
.map((a) =>
mapAsset(a, {
auth,
stripMetadata: false,
// ignore stacks for non partner users
withStack: a.ownerId === authUserId,
}),
),
deleted, deleted,
}; };
return result; return result;