diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 16ba34df5..f73b6bcdf 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -752,6 +752,25 @@ export const AudioCodec = { export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec]; +/** + * + * @export + * @interface AuditDeletesResponseDto + */ +export interface AuditDeletesResponseDto { + /** + * + * @type {Array} + * @memberof AuditDeletesResponseDto + */ + 'ids': Array; + /** + * + * @type {boolean} + * @memberof AuditDeletesResponseDto + */ + 'needsFullSync': boolean; +} /** * * @export @@ -1243,6 +1262,20 @@ export interface DownloadResponseDto { */ 'totalSize': number; } +/** + * + * @export + * @enum {string} + */ + +export const EntityType = { + Asset: 'ASSET', + Album: 'ALBUM' +} as const; + +export type EntityType = typeof EntityType[keyof typeof EntityType]; + + /** * * @export @@ -5120,11 +5153,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isArchived] * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] + * @param {string} [updatedAfter] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { + getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5166,6 +5200,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['skip'] = skip; } + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + if (ifNoneMatch != null) { localVarHeaderParameter['if-none-match'] = String(ifNoneMatch); } @@ -6274,12 +6314,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isArchived] * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] + * @param {string} [updatedAfter] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options); + async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, updatedAfter, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6576,7 +6617,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); + return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, /** * Get a single asset\'s information @@ -6895,6 +6936,13 @@ export interface AssetApiGetAllAssetsRequest { */ readonly skip?: number + /** + * + * @type {string} + * @memberof AssetApiGetAllAssets + */ + readonly updatedAfter?: string + /** * ETag of data already cached on the client * @type {string} @@ -7460,7 +7508,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } /** @@ -7671,6 +7719,163 @@ export class AssetApi extends BaseAPI { } +/** + * AuditApi - axios parameter creator + * @export + */ +export const AuditApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {EntityType} entityType + * @param {string} after + * @param {string} [userId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuditDeletes: async (entityType: EntityType, after: string, userId?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'entityType' is not null or undefined + assertParamExists('getAuditDeletes', 'entityType', entityType) + // verify required parameter 'after' is not null or undefined + assertParamExists('getAuditDeletes', 'after', after) + const localVarPath = `/audit/deletes`; + // 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 cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (entityType !== undefined) { + localVarQueryParameter['entityType'] = entityType; + } + + if (userId !== undefined) { + localVarQueryParameter['userId'] = userId; + } + + if (after !== undefined) { + localVarQueryParameter['after'] = (after as any instanceof Date) ? + (after as any).toISOString() : + after; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuditApi - functional programming interface + * @export + */ +export const AuditApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration) + return { + /** + * + * @param {EntityType} entityType + * @param {string} after + * @param {string} [userId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAuditDeletes(entityType: EntityType, after: string, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * AuditApi - factory interface + * @export + */ +export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuditApiFp(configuration) + return { + /** + * + * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getAuditDeletes operation in AuditApi. + * @export + * @interface AuditApiGetAuditDeletesRequest + */ +export interface AuditApiGetAuditDeletesRequest { + /** + * + * @type {EntityType} + * @memberof AuditApiGetAuditDeletes + */ + readonly entityType: EntityType + + /** + * + * @type {string} + * @memberof AuditApiGetAuditDeletes + */ + readonly after: string + + /** + * + * @type {string} + * @memberof AuditApiGetAuditDeletes + */ + readonly userId?: string +} + +/** + * AuditApi - object-oriented interface + * @export + * @class AuditApi + * @extends {BaseAPI} + */ +export class AuditApi extends BaseAPI { + /** + * + * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuditApi + */ + public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) { + return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * AuthenticationApi - axios parameter creator * @export diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 93b704976..506dfbf6a 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -29,6 +29,8 @@ doc/AssetResponseDto.md doc/AssetStatsResponseDto.md doc/AssetTypeEnum.md doc/AudioCodec.md +doc/AuditApi.md +doc/AuditDeletesResponseDto.md doc/AuthDeviceResponseDto.md doc/AuthenticationApi.md doc/BulkIdResponseDto.md @@ -50,6 +52,7 @@ doc/DeleteAssetStatus.md doc/DownloadArchiveInfo.md doc/DownloadInfoDto.md doc/DownloadResponseDto.md +doc/EntityType.md doc/ExifResponseDto.md doc/ImportAssetDto.md doc/JobApi.md @@ -134,6 +137,7 @@ lib/api.dart lib/api/album_api.dart lib/api/api_key_api.dart lib/api/asset_api.dart +lib/api/audit_api.dart lib/api/authentication_api.dart lib/api/job_api.dart lib/api/o_auth_api.dart @@ -176,6 +180,7 @@ lib/model/asset_response_dto.dart lib/model/asset_stats_response_dto.dart lib/model/asset_type_enum.dart lib/model/audio_codec.dart +lib/model/audit_deletes_response_dto.dart lib/model/auth_device_response_dto.dart lib/model/bulk_id_response_dto.dart lib/model/bulk_ids_dto.dart @@ -196,6 +201,7 @@ lib/model/delete_asset_status.dart lib/model/download_archive_info.dart lib/model/download_info_dto.dart lib/model/download_response_dto.dart +lib/model/entity_type.dart lib/model/exif_response_dto.dart lib/model/import_asset_dto.dart lib/model/job_command.dart @@ -292,6 +298,8 @@ test/asset_response_dto_test.dart test/asset_stats_response_dto_test.dart test/asset_type_enum_test.dart test/audio_codec_test.dart +test/audit_api_test.dart +test/audit_deletes_response_dto_test.dart test/auth_device_response_dto_test.dart test/authentication_api_test.dart test/bulk_id_response_dto_test.dart @@ -313,6 +321,7 @@ test/delete_asset_status_test.dart test/download_archive_info_test.dart test/download_info_dto_test.dart test/download_response_dto_test.dart +test/entity_type_test.dart test/exif_response_dto_test.dart test/import_asset_dto_test.dart test/job_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index fb3873a32..188c09f5f 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -113,6 +113,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} | *AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset | *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload | +*AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes | *AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | *AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices | @@ -204,6 +205,7 @@ Class | Method | HTTP request | Description - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) + - [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md) - [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdsDto](doc//BulkIdsDto.md) @@ -224,6 +226,7 @@ Class | Method | HTTP request | Description - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadResponseDto](doc//DownloadResponseDto.md) + - [EntityType](doc//EntityType.md) - [ExifResponseDto](doc//ExifResponseDto.md) - [ImportAssetDto](doc//ImportAssetDto.md) - [JobCommand](doc//JobCommand.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 2d76e5f9a..140f509b0 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -380,7 +380,7 @@ Name | Type | Description | Notes [[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) # **getAllAssets** -> List getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch) +> List getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, updatedAfter, ifNoneMatch) @@ -410,10 +410,11 @@ final isFavorite = true; // bool | final isArchived = true; // bool | final withoutThumbs = true; // bool | Include assets without thumbnails final skip = 8.14; // num | +final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final ifNoneMatch = ifNoneMatch_example; // String | ETag of data already cached on the client try { - final result = api_instance.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch); + final result = api_instance.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, updatedAfter, ifNoneMatch); print(result); } catch (e) { print('Exception when calling AssetApi->getAllAssets: $e\n'); @@ -429,6 +430,7 @@ Name | Type | Description | Notes **isArchived** | **bool**| | [optional] **withoutThumbs** | **bool**| Include assets without thumbnails | [optional] **skip** | **num**| | [optional] + **updatedAfter** | **DateTime**| | [optional] **ifNoneMatch** | **String**| ETag of data already cached on the client | [optional] ### Return type diff --git a/mobile/openapi/doc/AuditApi.md b/mobile/openapi/doc/AuditApi.md new file mode 100644 index 000000000..63a1c97a3 --- /dev/null +++ b/mobile/openapi/doc/AuditApi.md @@ -0,0 +1,73 @@ +# openapi.api.AuditApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**getAuditDeletes**](AuditApi.md#getauditdeletes) | **GET** /audit/deletes | + + +# **getAuditDeletes** +> AuditDeletesResponseDto getAuditDeletes(entityType, after, userId) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = AuditApi(); +final entityType = ; // EntityType | +final after = 2013-10-20T19:20:30+01:00; // DateTime | +final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + final result = api_instance.getAuditDeletes(entityType, after, userId); + print(result); +} catch (e) { + print('Exception when calling AuditApi->getAuditDeletes: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **entityType** | [**EntityType**](.md)| | + **after** | **DateTime**| | + **userId** | **String**| | [optional] + +### Return type + +[**AuditDeletesResponseDto**](AuditDeletesResponseDto.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) + diff --git a/mobile/openapi/doc/AuditDeletesResponseDto.md b/mobile/openapi/doc/AuditDeletesResponseDto.md new file mode 100644 index 000000000..c7c9594f1 --- /dev/null +++ b/mobile/openapi/doc/AuditDeletesResponseDto.md @@ -0,0 +1,16 @@ +# openapi.model.AuditDeletesResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**ids** | **List** | | [default to const []] +**needsFullSync** | **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) + + diff --git a/mobile/openapi/doc/EntityType.md b/mobile/openapi/doc/EntityType.md new file mode 100644 index 000000000..a01b3cf5e --- /dev/null +++ b/mobile/openapi/doc/EntityType.md @@ -0,0 +1,14 @@ +# openapi.model.EntityType + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 7797555bf..ec7655c28 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -31,6 +31,7 @@ part 'auth/http_bearer_auth.dart'; part 'api/api_key_api.dart'; part 'api/album_api.dart'; part 'api/asset_api.dart'; +part 'api/audit_api.dart'; part 'api/authentication_api.dart'; part 'api/job_api.dart'; part 'api/o_auth_api.dart'; @@ -66,6 +67,7 @@ part 'model/asset_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; +part 'model/audit_deletes_response_dto.dart'; part 'model/auth_device_response_dto.dart'; part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; @@ -86,6 +88,7 @@ part 'model/delete_asset_status.dart'; part 'model/download_archive_info.dart'; part 'model/download_info_dto.dart'; part 'model/download_response_dto.dart'; +part 'model/entity_type.dart'; part 'model/exif_response_dto.dart'; part 'model/import_asset_dto.dart'; part 'model/job_command.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 0f8b69a1b..a2f6b7b3a 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -358,9 +358,11 @@ class AssetApi { /// /// * [num] skip: /// + /// * [DateTime] updatedAfter: + /// /// * [String] ifNoneMatch: /// ETag of data already cached on the client - Future getAllAssetsWithHttpInfo({ String? userId, bool? isFavorite, bool? isArchived, bool? withoutThumbs, num? skip, String? ifNoneMatch, }) async { + Future getAllAssetsWithHttpInfo({ String? userId, bool? isFavorite, bool? isArchived, bool? withoutThumbs, num? skip, DateTime? updatedAfter, String? ifNoneMatch, }) async { // ignore: prefer_const_declarations final path = r'/asset'; @@ -386,6 +388,9 @@ class AssetApi { if (skip != null) { queryParams.addAll(_queryParams('', 'skip', skip)); } + if (updatedAfter != null) { + queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); + } if (ifNoneMatch != null) { headerParams[r'if-none-match'] = parameterToString(ifNoneMatch); @@ -420,10 +425,12 @@ class AssetApi { /// /// * [num] skip: /// + /// * [DateTime] updatedAfter: + /// /// * [String] ifNoneMatch: /// ETag of data already cached on the client - Future?> getAllAssets({ String? userId, bool? isFavorite, bool? isArchived, bool? withoutThumbs, num? skip, String? ifNoneMatch, }) async { - final response = await getAllAssetsWithHttpInfo( userId: userId, isFavorite: isFavorite, isArchived: isArchived, withoutThumbs: withoutThumbs, skip: skip, ifNoneMatch: ifNoneMatch, ); + Future?> getAllAssets({ String? userId, bool? isFavorite, bool? isArchived, bool? withoutThumbs, num? skip, DateTime? updatedAfter, String? ifNoneMatch, }) async { + final response = await getAllAssetsWithHttpInfo( userId: userId, isFavorite: isFavorite, isArchived: isArchived, withoutThumbs: withoutThumbs, skip: skip, updatedAfter: updatedAfter, ifNoneMatch: ifNoneMatch, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/audit_api.dart b/mobile/openapi/lib/api/audit_api.dart new file mode 100644 index 000000000..4eabd17c9 --- /dev/null +++ b/mobile/openapi/lib/api/audit_api.dart @@ -0,0 +1,79 @@ +// +// 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 AuditApi { + AuditApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'GET /audit/deletes' operation and returns the [Response]. + /// Parameters: + /// + /// * [EntityType] entityType (required): + /// + /// * [DateTime] after (required): + /// + /// * [String] userId: + Future getAuditDeletesWithHttpInfo(EntityType entityType, DateTime after, { String? userId, }) async { + // ignore: prefer_const_declarations + final path = r'/audit/deletes'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'entityType', entityType)); + if (userId != null) { + queryParams.addAll(_queryParams('', 'userId', userId)); + } + queryParams.addAll(_queryParams('', 'after', after)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [EntityType] entityType (required): + /// + /// * [DateTime] after (required): + /// + /// * [String] userId: + Future getAuditDeletes(EntityType entityType, DateTime after, { String? userId, }) async { + final response = await getAuditDeletesWithHttpInfo(entityType, after, userId: userId, ); + 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), 'AuditDeletesResponseDto',) as AuditDeletesResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 2a3ebeb64..702f89432 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -227,6 +227,8 @@ class ApiClient { return AssetTypeEnumTypeTransformer().decode(value); case 'AudioCodec': return AudioCodecTypeTransformer().decode(value); + case 'AuditDeletesResponseDto': + return AuditDeletesResponseDto.fromJson(value); case 'AuthDeviceResponseDto': return AuthDeviceResponseDto.fromJson(value); case 'BulkIdResponseDto': @@ -267,6 +269,8 @@ class ApiClient { return DownloadInfoDto.fromJson(value); case 'DownloadResponseDto': return DownloadResponseDto.fromJson(value); + case 'EntityType': + return EntityTypeTypeTransformer().decode(value); case 'ExifResponseDto': return ExifResponseDto.fromJson(value); case 'ImportAssetDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index a80daaf76..a9df71cfd 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -67,6 +67,9 @@ String parameterToString(dynamic value) { if (value is DeleteAssetStatus) { return DeleteAssetStatusTypeTransformer().encode(value).toString(); } + if (value is EntityType) { + return EntityTypeTypeTransformer().encode(value).toString(); + } if (value is JobCommand) { return JobCommandTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/audit_deletes_response_dto.dart b/mobile/openapi/lib/model/audit_deletes_response_dto.dart new file mode 100644 index 000000000..a0bc0dd4d --- /dev/null +++ b/mobile/openapi/lib/model/audit_deletes_response_dto.dart @@ -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 AuditDeletesResponseDto { + /// Returns a new [AuditDeletesResponseDto] instance. + AuditDeletesResponseDto({ + this.ids = const [], + required this.needsFullSync, + }); + + List ids; + + bool needsFullSync; + + @override + bool operator ==(Object other) => identical(this, other) || other is AuditDeletesResponseDto && + other.ids == ids && + other.needsFullSync == needsFullSync; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (ids.hashCode) + + (needsFullSync.hashCode); + + @override + String toString() => 'AuditDeletesResponseDto[ids=$ids, needsFullSync=$needsFullSync]'; + + Map toJson() { + final json = {}; + json[r'ids'] = this.ids; + json[r'needsFullSync'] = this.needsFullSync; + return json; + } + + /// Returns a new [AuditDeletesResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AuditDeletesResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AuditDeletesResponseDto( + ids: json[r'ids'] is List + ? (json[r'ids'] as List).cast() + : const [], + needsFullSync: mapValueOfType(json, r'needsFullSync')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AuditDeletesResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AuditDeletesResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AuditDeletesResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AuditDeletesResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'ids', + 'needsFullSync', + }; +} + diff --git a/mobile/openapi/lib/model/entity_type.dart b/mobile/openapi/lib/model/entity_type.dart new file mode 100644 index 000000000..1a45e1b2e --- /dev/null +++ b/mobile/openapi/lib/model/entity_type.dart @@ -0,0 +1,85 @@ +// +// 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 EntityType { + /// Instantiate a new enum with the provided [value]. + const EntityType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const ASSET = EntityType._(r'ASSET'); + static const ALBUM = EntityType._(r'ALBUM'); + + /// List of all possible values in this [enum][EntityType]. + static const values = [ + ASSET, + ALBUM, + ]; + + static EntityType? fromJson(dynamic value) => EntityTypeTypeTransformer().decode(value); + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = EntityType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [EntityType] to String, +/// and [decode] dynamic data back to [EntityType]. +class EntityTypeTypeTransformer { + factory EntityTypeTypeTransformer() => _instance ??= const EntityTypeTypeTransformer._(); + + const EntityTypeTypeTransformer._(); + + String encode(EntityType data) => data.value; + + /// Decodes a [dynamic value][data] to a EntityType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + EntityType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'ASSET': return EntityType.ASSET; + case r'ALBUM': return EntityType.ALBUM; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [EntityTypeTypeTransformer] instance. + static EntityTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index ef8ae195a..076521b0e 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -55,7 +55,7 @@ void main() { // Get all AssetEntity belong to the user // - //Future> getAllAssets({ String userId, bool isFavorite, bool isArchived, bool withoutThumbs, num skip, String ifNoneMatch }) async + //Future> getAllAssets({ String userId, bool isFavorite, bool isArchived, bool withoutThumbs, num skip, DateTime updatedAfter, String ifNoneMatch }) async test('test getAllAssets', () async { // TODO }); diff --git a/mobile/openapi/test/audit_api_test.dart b/mobile/openapi/test/audit_api_test.dart new file mode 100644 index 000000000..68ffede19 --- /dev/null +++ b/mobile/openapi/test/audit_api_test.dart @@ -0,0 +1,26 @@ +// +// 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 AuditApi +void main() { + // final instance = AuditApi(); + + group('tests for AuditApi', () { + //Future getAuditDeletes(EntityType entityType, DateTime after, { String userId }) async + test('test getAuditDeletes', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/audit_deletes_response_dto_test.dart b/mobile/openapi/test/audit_deletes_response_dto_test.dart new file mode 100644 index 000000000..45dbccc28 --- /dev/null +++ b/mobile/openapi/test/audit_deletes_response_dto_test.dart @@ -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 AuditDeletesResponseDto +void main() { + // final instance = AuditDeletesResponseDto(); + + group('test AuditDeletesResponseDto', () { + // List ids (default value: const []) + test('to test the property `ids`', () async { + // TODO + }); + + // bool needsFullSync + test('to test the property `needsFullSync`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/entity_type_test.dart b/mobile/openapi/test/entity_type_test.dart new file mode 100644 index 000000000..81f023308 --- /dev/null +++ b/mobile/openapi/test/entity_type_test.dart @@ -0,0 +1,21 @@ +// +// 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 EntityType +void main() { + + group('test EntityType', () { + + }); + +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 8b482a8e2..dfbfe66ce 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -769,6 +769,15 @@ "type": "number" } }, + { + "name": "updatedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, { "name": "if-none-match", "in": "header", @@ -2071,6 +2080,65 @@ ] } }, + "/audit/deletes": { + "get": { + "operationId": "getAuditDeletes", + "parameters": [ + { + "name": "entityType", + "required": true, + "in": "query", + "schema": { + "$ref": "#/components/schemas/EntityType" + } + }, + { + "name": "userId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "after", + "required": true, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuditDeletesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Audit" + ] + } + }, "/auth/admin-sign-up": { "post": { "operationId": "adminSignUp", @@ -5239,6 +5307,24 @@ ], "type": "string" }, + "AuditDeletesResponseDto": { + "properties": { + "ids": { + "items": { + "type": "string" + }, + "type": "array" + }, + "needsFullSync": { + "type": "boolean" + } + }, + "required": [ + "needsFullSync", + "ids" + ], + "type": "object" + }, "AuthDeviceResponseDto": { "properties": { "createdAt": { @@ -5701,6 +5787,13 @@ ], "type": "object" }, + "EntityType": { + "enum": [ + "ASSET", + "ALBUM" + ], + "type": "string" + }, "ExifResponseDto": { "properties": { "city": { diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 59c161f67..9de81a4f0 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -13,7 +13,7 @@ import { import { when } from 'jest-when'; import { Readable } from 'stream'; import { ICryptoRepository } from '../crypto'; -import { IJobRepository, JobName } from '../index'; +import { IJobRepository, JobName } from '../job'; import { IStorageRepository } from '../storage'; import { AssetStats, IAssetRepository } from './asset.repository'; import { AssetService, UploadFieldName } from './asset.service'; diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index c7dc79046..8f681d9bd 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -16,18 +16,23 @@ import { AssetIdsDto, AssetJobName, AssetJobsDto, + AssetStatsDto, DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto, + MapMarkerDto, + mapStats, MemoryLaneDto, TimeBucketAssetDto, TimeBucketDto, } from './dto'; -import { AssetStatsDto, mapStats } from './dto/asset-statistics.dto'; -import { MapMarkerDto } from './dto/map-marker.dto'; -import { AssetResponseDto, mapAsset, MapMarkerResponseDto } from './response-dto'; -import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto'; -import { TimeBucketResponseDto } from './response-dto/time-bucket-response.dto'; +import { + AssetResponseDto, + mapAsset, + MapMarkerResponseDto, + MemoryLaneResponseDto, + TimeBucketResponseDto, +} from './response-dto'; export enum UploadFieldName { ASSET_DATA = 'assetData', diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index 226cc77a9..1a018c233 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -84,3 +84,8 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { checksum: entity.checksum.toString('base64'), }; } + +export class MemoryLaneResponseDto { + title!: string; + assets!: AssetResponseDto[]; +} diff --git a/server/src/domain/asset/response-dto/memory-lane-response.dto.ts b/server/src/domain/asset/response-dto/memory-lane-response.dto.ts deleted file mode 100644 index 875a0d5b7..000000000 --- a/server/src/domain/asset/response-dto/memory-lane-response.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AssetResponseDto } from './asset-response.dto'; - -export class MemoryLaneResponseDto { - title!: string; - assets!: AssetResponseDto[]; -} diff --git a/server/src/domain/audit/audi.service.spec.ts b/server/src/domain/audit/audi.service.spec.ts new file mode 100644 index 000000000..32601caf0 --- /dev/null +++ b/server/src/domain/audit/audi.service.spec.ts @@ -0,0 +1,61 @@ +import { DatabaseAction, EntityType } from '@app/infra/entities'; +import { auditStub, authStub, IAccessRepositoryMock, newAccessRepositoryMock, newAuditRepositoryMock } from '@test'; +import { IAuditRepository } from './audit.repository'; +import { AuditService } from './audit.service'; + +describe(AuditService.name, () => { + let sut: AuditService; + let accessMock: IAccessRepositoryMock; + let auditMock: jest.Mocked; + + beforeEach(async () => { + accessMock = newAccessRepositoryMock(); + auditMock = newAuditRepositoryMock(); + sut = new AuditService(accessMock, auditMock); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('handleCleanup', () => { + it('should delete old audit entries', async () => { + await expect(sut.handleCleanup()).resolves.toBe(true); + expect(auditMock.removeBefore).toBeCalledWith(expect.any(Date)); + }); + }); + + describe('getDeletes', () => { + it('should require full sync if the request is older than 100 days', async () => { + auditMock.getAfter.mockResolvedValue([]); + + const date = new Date(2022, 0, 1); + await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({ + needsFullSync: true, + ids: [], + }); + + expect(auditMock.getAfter).toHaveBeenCalledWith(date, { + action: DatabaseAction.DELETE, + ownerId: authStub.admin.id, + entityType: EntityType.ASSET, + }); + }); + + it('should get any new or updated assets and deleted ids', async () => { + auditMock.getAfter.mockResolvedValue([auditStub.delete]); + + const date = new Date(); + await expect(sut.getDeletes(authStub.admin, { after: date, entityType: EntityType.ASSET })).resolves.toEqual({ + needsFullSync: false, + ids: ['asset-deleted'], + }); + + expect(auditMock.getAfter).toHaveBeenCalledWith(date, { + action: DatabaseAction.DELETE, + ownerId: authStub.admin.id, + entityType: EntityType.ASSET, + }); + }); + }); +}); diff --git a/server/src/domain/audit/audit.dto.ts b/server/src/domain/audit/audit.dto.ts new file mode 100644 index 000000000..2494883e0 --- /dev/null +++ b/server/src/domain/audit/audit.dto.ts @@ -0,0 +1,24 @@ +import { EntityType } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsDate, IsEnum, IsOptional, IsUUID } from 'class-validator'; + +export class AuditDeletesDto { + @IsDate() + @Type(() => Date) + after!: Date; + + @ApiProperty({ enum: EntityType, enumName: 'EntityType' }) + @IsEnum(EntityType) + entityType!: EntityType; + + @IsOptional() + @IsUUID('4') + @ApiProperty({ format: 'uuid' }) + userId?: string; +} + +export class AuditDeletesResponseDto { + needsFullSync!: boolean; + ids!: string[]; +} diff --git a/server/src/domain/audit/audit.repository.ts b/server/src/domain/audit/audit.repository.ts new file mode 100644 index 000000000..774ab1e42 --- /dev/null +++ b/server/src/domain/audit/audit.repository.ts @@ -0,0 +1,14 @@ +import { AuditEntity, DatabaseAction, EntityType } from '@app/infra/entities'; + +export const IAuditRepository = 'IAuditRepository'; + +export interface AuditSearch { + action?: DatabaseAction; + entityType?: EntityType; + ownerId?: string; +} + +export interface IAuditRepository { + getAfter(since: Date, options: AuditSearch): Promise; + removeBefore(before: Date): Promise; +} diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts new file mode 100644 index 000000000..47d98e688 --- /dev/null +++ b/server/src/domain/audit/audit.service.ts @@ -0,0 +1,43 @@ +import { DatabaseAction } from '@app/infra/entities'; +import { Inject, Injectable } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import { AccessCore, IAccessRepository, Permission } from '../access'; +import { AuthUserDto } from '../auth'; +import { AUDIT_LOG_MAX_DURATION } from '../domain.constant'; +import { AuditDeletesDto, AuditDeletesResponseDto } from './audit.dto'; +import { IAuditRepository } from './audit.repository'; + +@Injectable() +export class AuditService { + private access: AccessCore; + + constructor( + @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAuditRepository) private repository: IAuditRepository, + ) { + this.access = new AccessCore(accessRepository); + } + + async handleCleanup(): Promise { + await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); + return true; + } + + async getDeletes(authUser: AuthUserDto, dto: AuditDeletesDto): Promise { + const userId = dto.userId || authUser.id; + await this.access.requirePermission(authUser, Permission.LIBRARY_READ, userId); + + const audits = await this.repository.getAfter(dto.after, { + ownerId: userId, + entityType: dto.entityType, + action: DatabaseAction.DELETE, + }); + + const duration = DateTime.now().diff(DateTime.fromJSDate(dto.after)); + + return { + needsFullSync: duration > AUDIT_LOG_MAX_DURATION, + ids: audits.map(({ entityId }) => entityId), + }; + } +} diff --git a/server/src/domain/audit/index.ts b/server/src/domain/audit/index.ts new file mode 100644 index 000000000..2074b86f3 --- /dev/null +++ b/server/src/domain/audit/index.ts @@ -0,0 +1,3 @@ +export * from './audit.dto'; +export * from './audit.repository'; +export * from './audit.service'; diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index 7b60b796a..04b830976 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -1,8 +1,11 @@ import { AssetType } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; +import { Duration } from 'luxon'; import { extname } from 'node:path'; import pkg from 'src/../../package.json'; +export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 }); + const [major, minor, patch] = pkg.version.split('.'); export interface IServerVersion { diff --git a/server/src/domain/domain.module.ts b/server/src/domain/domain.module.ts index 72d530083..a2efd8796 100644 --- a/server/src/domain/domain.module.ts +++ b/server/src/domain/domain.module.ts @@ -2,6 +2,7 @@ import { DynamicModule, Global, Module, ModuleMetadata, OnApplicationShutdown, P import { AlbumService } from './album'; import { APIKeyService } from './api-key'; import { AssetService } from './asset'; +import { AuditService } from './audit'; import { AuthService } from './auth'; import { FacialRecognitionService } from './facial-recognition'; import { JobService } from './job'; @@ -23,6 +24,7 @@ const providers: Provider[] = [ AlbumService, APIKeyService, AssetService, + AuditService, AuthService, FacialRecognitionService, JobService, diff --git a/server/src/domain/index.ts b/server/src/domain/index.ts index 201e31ff3..c66c4eadc 100644 --- a/server/src/domain/index.ts +++ b/server/src/domain/index.ts @@ -2,6 +2,7 @@ export * from './access'; export * from './album'; export * from './api-key'; export * from './asset'; +export * from './audit'; export * from './auth'; export * from './communication'; export * from './crypto'; diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index 02fa588c9..7062ab86b 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -55,6 +55,7 @@ export enum JobName { // cleanup DELETE_FILES = 'delete-files', + CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', // search SEARCH_INDEX_ASSETS = 'search-index-assets', @@ -84,6 +85,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK, [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK, [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK, + [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK, [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK, // conversion diff --git a/server/src/domain/job/job.repository.ts b/server/src/domain/job/job.repository.ts index f605bef4b..a452ad4f9 100644 --- a/server/src/domain/job/job.repository.ts +++ b/server/src/domain/job/job.repository.ts @@ -68,6 +68,9 @@ export type JobItem = // Filesystem | { name: JobName.DELETE_FILES; data: IDeleteFilesJob } + // Audit log cleanup + | { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob } + // Asset Deletion | { name: JobName.PERSON_CLEANUP; data?: IBaseJob } diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index 503440a5c..f8a323bbb 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -51,6 +51,7 @@ describe(JobService.name, () => { [{ name: JobName.USER_DELETE_CHECK }], [{ name: JobName.PERSON_CLEANUP }], [{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }], + [{ name: JobName.CLEAN_OLD_AUDIT_LOGS }], ]); }); }); diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 1c8290891..cc90e4ccd 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -136,6 +136,7 @@ export class JobService { await this.jobRepository.queue({ name: JobName.USER_DELETE_CHECK }); await this.jobRepository.queue({ name: JobName.PERSON_CLEANUP }); await this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }); + await this.jobRepository.queue({ name: JobName.CLEAN_OLD_AUDIT_LOGS }); } /** diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index ce85bd369..b19b33632 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -1,7 +1,7 @@ import { AssetEntity, ExifEntity } from '@app/infra/entities'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Not } from 'typeorm'; +import { IsNull, MoreThan, Not } from 'typeorm'; import { In } from 'typeorm/find-options/operator/In'; import { Repository } from 'typeorm/repository/Repository'; import { AssetSearchDto } from './dto/asset-search.dto'; @@ -131,6 +131,7 @@ export class AssetRepository implements IAssetRepository { isVisible: true, isFavorite: dto.isFavorite, isArchived: dto.isArchived, + updatedAt: dto.updatedAfter ? MoreThan(dto.updatedAfter) : undefined, }, relations: { exifInfo: true, diff --git a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts index a629c915c..72d16edbe 100644 --- a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts +++ b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts @@ -1,7 +1,7 @@ import { toBoolean } from '@app/domain'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { IsBoolean, IsDate, IsNotEmpty, IsNumber, IsOptional, IsUUID } from 'class-validator'; export class AssetSearchDto { @IsOptional() @@ -32,4 +32,9 @@ export class AssetSearchDto { @IsUUID('4') @ApiProperty({ format: 'uuid' }) userId?: string; + + @IsOptional() + @IsDate() + @Type(() => Date) + updatedAfter?: Date; } diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index 1067485ac..6b08228bd 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -16,6 +16,7 @@ import { APIKeyController, AppController, AssetController, + AuditController, AuthController, JobController, OAuthController, @@ -42,6 +43,7 @@ import { AppController, AlbumController, APIKeyController, + AuditController, AuthController, JobController, OAuthController, diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 652238a1c..7a69c34e8 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -9,14 +9,14 @@ import { AuthUserDto, DownloadInfoDto, DownloadResponseDto, + MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, + MemoryLaneResponseDto, TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto, } from '@app/domain'; -import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto'; -import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto'; import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard'; diff --git a/server/src/immich/controllers/audit.controller.ts b/server/src/immich/controllers/audit.controller.ts new file mode 100644 index 000000000..8b28f6e9f --- /dev/null +++ b/server/src/immich/controllers/audit.controller.ts @@ -0,0 +1,18 @@ +import { AuditDeletesDto, AuditDeletesResponseDto, AuditService, AuthUserDto } from '@app/domain'; +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Authenticated, AuthUser } from '../app.guard'; +import { UseValidation } from '../app.utils'; + +@ApiTags('Audit') +@Controller('audit') +@Authenticated() +@UseValidation() +export class AuditController { + constructor(private service: AuditService) {} + + @Get('deletes') + getAuditDeletes(@AuthUser() authUser: AuthUserDto, @Query() dto: AuditDeletesDto): Promise { + return this.service.getDeletes(authUser, dto); + } +} diff --git a/server/src/immich/controllers/index.ts b/server/src/immich/controllers/index.ts index e257c5a9a..b28e82ecb 100644 --- a/server/src/immich/controllers/index.ts +++ b/server/src/immich/controllers/index.ts @@ -2,6 +2,7 @@ export * from './album.controller'; export * from './api-key.controller'; export * from './app.controller'; export * from './asset.controller'; +export * from './audit.controller'; export * from './auth.controller'; export * from './job.controller'; export * from './oauth.controller'; diff --git a/server/src/infra/database.config.ts b/server/src/infra/database.config.ts index 8df877705..089fd6878 100644 --- a/server/src/infra/database.config.ts +++ b/server/src/infra/database.config.ts @@ -17,6 +17,7 @@ export const databaseConfig: PostgresConnectionOptions = { entities: [__dirname + '/entities/*.entity.{js,ts}'], synchronize: false, migrations: [__dirname + '/migrations/*.{js,ts}'], + subscribers: [__dirname + '/subscribers/*.{js,ts}'], migrationsRun: true, connectTimeoutMS: 10000, // 10 seconds ...urlOrParts, diff --git a/server/src/infra/entities/audit.entity.ts b/server/src/infra/entities/audit.entity.ts new file mode 100644 index 000000000..be5e14891 --- /dev/null +++ b/server/src/infra/entities/audit.entity.ts @@ -0,0 +1,34 @@ +import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; + +export enum DatabaseAction { + CREATE = 'CREATE', + UPDATE = 'UPDATE', + DELETE = 'DELETE', +} + +export enum EntityType { + ASSET = 'ASSET', + ALBUM = 'ALBUM', +} + +@Entity('audit') +@Index('IDX_ownerId_createdAt', ['ownerId', 'createdAt']) +export class AuditEntity { + @PrimaryGeneratedColumn('increment') + id!: number; + + @Column() + entityType!: EntityType; + + @Column({ type: 'uuid' }) + entityId!: string; + + @Column() + action!: DatabaseAction; + + @Column({ type: 'uuid' }) + ownerId!: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; +} diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts index 6864a3f73..632a8d6b4 100644 --- a/server/src/infra/entities/index.ts +++ b/server/src/infra/entities/index.ts @@ -2,6 +2,7 @@ import { AlbumEntity } from './album.entity'; import { APIKeyEntity } from './api-key.entity'; import { AssetFaceEntity } from './asset-face.entity'; import { AssetEntity } from './asset.entity'; +import { AuditEntity } from './audit.entity'; import { PartnerEntity } from './partner.entity'; import { PersonEntity } from './person.entity'; import { SharedLinkEntity } from './shared-link.entity'; @@ -15,6 +16,7 @@ export * from './album.entity'; export * from './api-key.entity'; export * from './asset-face.entity'; export * from './asset.entity'; +export * from './audit.entity'; export * from './exif.entity'; export * from './partner.entity'; export * from './person.entity'; @@ -30,6 +32,7 @@ export const databaseEntities = [ APIKeyEntity, AssetEntity, AssetFaceEntity, + AuditEntity, PartnerEntity, PersonEntity, SharedLinkEntity, diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index 060c64ae3..98d4387eb 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -2,6 +2,7 @@ import { IAccessRepository, IAlbumRepository, IAssetRepository, + IAuditRepository, ICommunicationRepository, ICryptoRepository, IFaceRepository, @@ -35,6 +36,7 @@ import { AlbumRepository, APIKeyRepository, AssetRepository, + AuditRepository, CommunicationRepository, CryptoRepository, FaceRepository, @@ -58,6 +60,7 @@ const providers: Provider[] = [ { provide: IAccessRepository, useClass: AccessRepository }, { provide: IAlbumRepository, useClass: AlbumRepository }, { provide: IAssetRepository, useClass: AssetRepository }, + { provide: IAuditRepository, useClass: AuditRepository }, { provide: ICommunicationRepository, useClass: CommunicationRepository }, { provide: ICryptoRepository, useClass: CryptoRepository }, { provide: IFaceRepository, useClass: FaceRepository }, diff --git a/server/src/infra/migrations/1692804658140-AddAuditTable.ts b/server/src/infra/migrations/1692804658140-AddAuditTable.ts new file mode 100644 index 000000000..71b8c7b2c --- /dev/null +++ b/server/src/infra/migrations/1692804658140-AddAuditTable.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAuditTable1692804658140 implements MigrationInterface { + name = 'AddAuditTable1692804658140' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "audit" ("id" SERIAL NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_ownerId_createdAt" ON "audit" ("ownerId", "createdAt") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_ownerId_createdAt"`); + await queryRunner.query(`DROP TABLE "audit"`); + } + +} diff --git a/server/src/infra/repositories/audit.repository.ts b/server/src/infra/repositories/audit.repository.ts new file mode 100644 index 000000000..b19d38577 --- /dev/null +++ b/server/src/infra/repositories/audit.repository.ts @@ -0,0 +1,26 @@ +import { AuditSearch, IAuditRepository } from '@app/domain'; +import { InjectRepository } from '@nestjs/typeorm'; +import { LessThan, MoreThan, Repository } from 'typeorm'; +import { AuditEntity } from '../entities'; + +export class AuditRepository implements IAuditRepository { + constructor(@InjectRepository(AuditEntity) private repository: Repository) {} + + getAfter(since: Date, options: AuditSearch): Promise { + return this.repository + .createQueryBuilder('audit') + .where({ + createdAt: MoreThan(since), + action: options.action, + entityType: options.entityType, + ownerId: options.ownerId, + }) + .distinctOn(['audit.entityId', 'audit.entityType']) + .orderBy('audit.entityId, audit.entityType, audit.createdAt', 'DESC') + .getMany(); + } + + async removeBefore(before: Date): Promise { + await this.repository.delete({ createdAt: LessThan(before) }); + } +} diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 5c7261b2d..c52c350fb 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -2,6 +2,7 @@ export * from './access.repository'; export * from './album.repository'; export * from './api-key.repository'; export * from './asset.repository'; +export * from './audit.repository'; export * from './communication.repository'; export * from './crypto.repository'; export * from './face.repository'; diff --git a/server/src/infra/subscribers/audit.subscriber.ts b/server/src/infra/subscribers/audit.subscriber.ts new file mode 100644 index 000000000..c0e831307 --- /dev/null +++ b/server/src/infra/subscribers/audit.subscriber.ts @@ -0,0 +1,38 @@ +import { EntitySubscriberInterface, EventSubscriber, RemoveEvent } from 'typeorm'; +import { AlbumEntity, AssetEntity, AuditEntity, DatabaseAction, EntityType } from '../entities'; + +@EventSubscriber() +export class AuditSubscriber implements EntitySubscriberInterface { + async afterRemove(event: RemoveEvent): Promise { + await this.onEvent(DatabaseAction.DELETE, event); + } + + private async onEvent(action: DatabaseAction, event: RemoveEvent): Promise { + const audit = this.getAudit(event.metadata.name, { ...event.entity, id: event.entityId }); + if (audit && audit.entityId && audit.ownerId) { + await event.manager.getRepository(AuditEntity).save({ ...audit, action }); + } + } + + private getAudit(entityName: string, entity: any): Partial | null { + switch (entityName) { + case AssetEntity.name: + const asset = entity as AssetEntity; + return { + entityType: EntityType.ASSET, + entityId: asset.id, + ownerId: asset.ownerId, + }; + + case AlbumEntity.name: + const album = entity as AlbumEntity; + return { + entityType: EntityType.ALBUM, + entityId: album.id, + ownerId: album.ownerId, + }; + } + + return null; + } +} diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 1204a6ebd..32d3c17b4 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -1,4 +1,5 @@ import { + AuditService, FacialRecognitionService, IDeleteFilesJob, JobName, @@ -35,11 +36,13 @@ export class AppService { private storageService: StorageService, private systemConfigService: SystemConfigService, private userService: UserService, + private auditService: AuditService, ) {} async init() { await this.jobService.registerHandlers({ [JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data), + [JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(), [JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(), [JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data), [JobName.QUEUE_OBJECT_TAGGING]: (data) => this.smartInfoService.handleQueueObjectTagging(data), diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts index 7c58f7102..5d421ebfd 100644 --- a/server/src/microservices/processors/metadata-extraction.processor.ts +++ b/server/src/microservices/processors/metadata-extraction.processor.ts @@ -408,7 +408,11 @@ export class MetadataExtractionProcessor { } await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] }); - await this.assetRepository.save({ id: asset.id, fileCreatedAt: fileCreatedAt || undefined }); + await this.assetRepository.save({ + id: asset.id, + fileCreatedAt: fileCreatedAt || undefined, + updatedAt: new Date(), + }); return true; } diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts new file mode 100644 index 000000000..c915ed821 --- /dev/null +++ b/server/test/fixtures/audit.stub.ts @@ -0,0 +1,29 @@ +import { AuditEntity, DatabaseAction, EntityType } from '@app/infra/entities'; +import { authStub } from './auth.stub'; + +export const auditStub = { + create: Object.freeze({ + id: 1, + entityId: 'asset-created', + action: DatabaseAction.CREATE, + entityType: EntityType.ASSET, + ownerId: authStub.admin.id, + createdAt: new Date(), + }), + update: Object.freeze({ + id: 2, + entityId: 'asset-updated', + action: DatabaseAction.UPDATE, + entityType: EntityType.ASSET, + ownerId: authStub.admin.id, + createdAt: new Date(), + }), + delete: Object.freeze({ + id: 3, + entityId: 'asset-deleted', + action: DatabaseAction.DELETE, + entityType: EntityType.ASSET, + ownerId: authStub.admin.id, + createdAt: new Date(), + }), +}; diff --git a/server/test/fixtures/index.ts b/server/test/fixtures/index.ts index c0e8aed3c..624cc0758 100644 --- a/server/test/fixtures/index.ts +++ b/server/test/fixtures/index.ts @@ -1,6 +1,7 @@ export * from './album.stub'; export * from './api-key.stub'; export * from './asset.stub'; +export * from './audit.stub'; export * from './auth.stub'; export * from './device.stub'; export * from './error.stub'; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 1c8d09c50..ad2f68ca5 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,10 +1,10 @@ import { IAccessRepository } from '@app/domain'; -export type IAccessRepositoryMock = { +export interface IAccessRepositoryMock { asset: jest.Mocked; album: jest.Mocked; library: jest.Mocked; -}; +} export const newAccessRepositoryMock = (): IAccessRepositoryMock => { return { diff --git a/server/test/repositories/audit.repository.mock.ts b/server/test/repositories/audit.repository.mock.ts new file mode 100644 index 000000000..bd1a4b815 --- /dev/null +++ b/server/test/repositories/audit.repository.mock.ts @@ -0,0 +1,8 @@ +import { IAuditRepository } from '@app/domain'; + +export const newAuditRepositoryMock = (): jest.Mocked => { + return { + getAfter: jest.fn(), + removeBefore: jest.fn(), + }; +}; diff --git a/server/test/repositories/index.ts b/server/test/repositories/index.ts index aa62a5f01..2b2c19026 100644 --- a/server/test/repositories/index.ts +++ b/server/test/repositories/index.ts @@ -2,6 +2,7 @@ export * from './access.repository.mock'; export * from './album.repository.mock'; export * from './api-key.repository.mock'; export * from './asset.repository.mock'; +export * from './audit.repository.mock'; export * from './communication.repository.mock'; export * from './crypto.repository.mock'; export * from './face.repository.mock'; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 16ba34df5..f73b6bcdf 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -752,6 +752,25 @@ export const AudioCodec = { export type AudioCodec = typeof AudioCodec[keyof typeof AudioCodec]; +/** + * + * @export + * @interface AuditDeletesResponseDto + */ +export interface AuditDeletesResponseDto { + /** + * + * @type {Array} + * @memberof AuditDeletesResponseDto + */ + 'ids': Array; + /** + * + * @type {boolean} + * @memberof AuditDeletesResponseDto + */ + 'needsFullSync': boolean; +} /** * * @export @@ -1243,6 +1262,20 @@ export interface DownloadResponseDto { */ 'totalSize': number; } +/** + * + * @export + * @enum {string} + */ + +export const EntityType = { + Asset: 'ASSET', + Album: 'ALBUM' +} as const; + +export type EntityType = typeof EntityType[keyof typeof EntityType]; + + /** * * @export @@ -5120,11 +5153,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isArchived] * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] + * @param {string} [updatedAfter] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { + getAllAssets: async (userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5166,6 +5200,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['skip'] = skip; } + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + if (ifNoneMatch != null) { localVarHeaderParameter['if-none-match'] = String(ifNoneMatch); } @@ -6274,12 +6314,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isArchived] * @param {boolean} [withoutThumbs] Include assets without thumbnails * @param {number} [skip] + * @param {string} [updatedAfter] * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, ifNoneMatch, options); + async getAllAssets(userId?: string, isFavorite?: boolean, isArchived?: boolean, withoutThumbs?: boolean, skip?: number, updatedAfter?: string, ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(userId, isFavorite, isArchived, withoutThumbs, skip, updatedAfter, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -6576,7 +6617,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); + return localVarFp.getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, /** * Get a single asset\'s information @@ -6895,6 +6936,13 @@ export interface AssetApiGetAllAssetsRequest { */ readonly skip?: number + /** + * + * @type {string} + * @memberof AssetApiGetAllAssets + */ + readonly updatedAfter?: string + /** * ETag of data already cached on the client * @type {string} @@ -7460,7 +7508,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getAllAssets(requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.withoutThumbs, requestParameters.skip, requestParameters.updatedAfter, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } /** @@ -7671,6 +7719,163 @@ export class AssetApi extends BaseAPI { } +/** + * AuditApi - axios parameter creator + * @export + */ +export const AuditApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {EntityType} entityType + * @param {string} after + * @param {string} [userId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuditDeletes: async (entityType: EntityType, after: string, userId?: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'entityType' is not null or undefined + assertParamExists('getAuditDeletes', 'entityType', entityType) + // verify required parameter 'after' is not null or undefined + assertParamExists('getAuditDeletes', 'after', after) + const localVarPath = `/audit/deletes`; + // 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 cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (entityType !== undefined) { + localVarQueryParameter['entityType'] = entityType; + } + + if (userId !== undefined) { + localVarQueryParameter['userId'] = userId; + } + + if (after !== undefined) { + localVarQueryParameter['after'] = (after as any instanceof Date) ? + (after as any).toISOString() : + after; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuditApi - functional programming interface + * @export + */ +export const AuditApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuditApiAxiosParamCreator(configuration) + return { + /** + * + * @param {EntityType} entityType + * @param {string} after + * @param {string} [userId] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAuditDeletes(entityType: EntityType, after: string, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAuditDeletes(entityType, after, userId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * AuditApi - factory interface + * @export + */ +export const AuditApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuditApiFp(configuration) + return { + /** + * + * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getAuditDeletes operation in AuditApi. + * @export + * @interface AuditApiGetAuditDeletesRequest + */ +export interface AuditApiGetAuditDeletesRequest { + /** + * + * @type {EntityType} + * @memberof AuditApiGetAuditDeletes + */ + readonly entityType: EntityType + + /** + * + * @type {string} + * @memberof AuditApiGetAuditDeletes + */ + readonly after: string + + /** + * + * @type {string} + * @memberof AuditApiGetAuditDeletes + */ + readonly userId?: string +} + +/** + * AuditApi - object-oriented interface + * @export + * @class AuditApi + * @extends {BaseAPI} + */ +export class AuditApi extends BaseAPI { + /** + * + * @param {AuditApiGetAuditDeletesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuditApi + */ + public getAuditDeletes(requestParameters: AuditApiGetAuditDeletesRequest, options?: AxiosRequestConfig) { + return AuditApiFp(this.configuration).getAuditDeletes(requestParameters.entityType, requestParameters.after, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * AuthenticationApi - axios parameter creator * @export