mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	refactor(server): move checkExistingAssets(), checkBulkUpdate() remove getAllAssets() (#9715)
* Refactor controller methods, non-breaking change * Remove getAllAssets * used imports * sync:sql * missing mock * Removing remaining references * chore: remove unused code --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									95012dc19b
								
							
						
					
					
						commit
						d5cf8e4bfe
					
				| @ -699,27 +699,6 @@ describe('/asset', () => { | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('GET /asset', () => { | ||||
|     it('should return stack data', async () => { | ||||
|       const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`); | ||||
| 
 | ||||
|       const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id); | ||||
| 
 | ||||
|       expect(status).toBe(200); | ||||
|       expect(stack).toEqual( | ||||
|         expect.objectContaining({ | ||||
|           stackCount: 3, | ||||
|           stack: | ||||
|             // Response includes children at the root level
 | ||||
|             expect.arrayContaining([ | ||||
|               expect.objectContaining({ id: stackAssets[1].id }), | ||||
|               expect.objectContaining({ id: stackAssets[2].id }), | ||||
|             ]), | ||||
|         }), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('PUT /asset', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(app).put('/asset'); | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { LoginResponseDto, getAllAssets } from '@immich/sdk'; | ||||
| import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk'; | ||||
| import { Socket } from 'socket.io-client'; | ||||
| import { errorDto } from 'src/responses'; | ||||
| import { app, asBearerAuth, utils } from 'src/utils'; | ||||
| @ -31,16 +31,16 @@ describe('/trash', () => { | ||||
|       const { id: assetId } = await utils.createAsset(admin.accessToken); | ||||
|       await utils.deleteAssets(admin.accessToken, [assetId]); | ||||
| 
 | ||||
|       const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]); | ||||
|       const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); | ||||
| 
 | ||||
|       const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); | ||||
|       expect(status).toBe(204); | ||||
| 
 | ||||
|       await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); | ||||
| 
 | ||||
|       const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(after.length).toBe(0); | ||||
|       const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(after.total).toBe(0); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -56,14 +56,14 @@ describe('/trash', () => { | ||||
|       const { id: assetId } = await utils.createAsset(admin.accessToken); | ||||
|       await utils.deleteAssets(admin.accessToken, [assetId]); | ||||
| 
 | ||||
|       const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(before).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: true })]); | ||||
|       const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true })); | ||||
| 
 | ||||
|       const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); | ||||
|       expect(status).toBe(204); | ||||
| 
 | ||||
|       const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(after).toStrictEqual([expect.objectContaining({ id: assetId, isTrashed: false })]); | ||||
|       const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); | ||||
|       expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false })); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk'; | ||||
| import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk'; | ||||
| import { readFileSync } from 'node:fs'; | ||||
| import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; | ||||
| import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; | ||||
| @ -28,8 +28,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(1); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('should skip a duplicate file', async () => { | ||||
| @ -40,8 +40,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(first.exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(1); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(1); | ||||
| 
 | ||||
|       const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); | ||||
|       expect(second.stderr).toBe(''); | ||||
| @ -60,8 +60,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')])); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should have accurate dry run', async () => { | ||||
| @ -76,8 +76,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('dry run should handle duplicates', async () => { | ||||
| @ -88,8 +88,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(first.exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(1); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(1); | ||||
| 
 | ||||
|       const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']); | ||||
|       expect(second.stderr).toBe(''); | ||||
| @ -112,8 +112,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(9); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(9); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -135,8 +135,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toBe(''); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(9); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(9); | ||||
| 
 | ||||
|       const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); | ||||
|       expect(albums.length).toBe(1); | ||||
| @ -151,8 +151,8 @@ describe(`immich upload`, () => { | ||||
|       expect(response1.stderr).toBe(''); | ||||
|       expect(response1.exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets1.length).toBe(9); | ||||
|       const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets1.total).toBe(9); | ||||
| 
 | ||||
|       const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) }); | ||||
|       expect(albums1.length).toBe(0); | ||||
| @ -167,8 +167,8 @@ describe(`immich upload`, () => { | ||||
|       expect(response2.stderr).toBe(''); | ||||
|       expect(response2.exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets2.length).toBe(9); | ||||
|       const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets2.total).toBe(9); | ||||
| 
 | ||||
|       const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) }); | ||||
|       expect(albums2.length).toBe(1); | ||||
| @ -193,8 +193,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toBe(''); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
| 
 | ||||
|       const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); | ||||
|       expect(albums.length).toBe(0); | ||||
| @ -219,8 +219,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toBe(''); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(9); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(9); | ||||
| 
 | ||||
|       const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); | ||||
|       expect(albums.length).toBe(1); | ||||
| @ -245,8 +245,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toBe(''); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
| 
 | ||||
|       const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); | ||||
|       expect(albums.length).toBe(0); | ||||
| @ -276,8 +276,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toBe(''); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(9); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(9); | ||||
|     }); | ||||
| 
 | ||||
|     it('should have accurate dry run', async () => { | ||||
| @ -302,8 +302,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toBe(''); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -328,8 +328,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(1); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw an error if attempting dry run', async () => { | ||||
| @ -344,8 +344,8 @@ describe(`immich upload`, () => { | ||||
|       expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`); | ||||
|       expect(exitCode).not.toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
| @ -367,8 +367,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(9); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(9); | ||||
|     }); | ||||
| 
 | ||||
|     it('should reject string argument', async () => { | ||||
| @ -408,8 +408,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(8); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(8); | ||||
|     }); | ||||
| 
 | ||||
|     it('should ignore assets matching glob pattern', async () => { | ||||
| @ -429,8 +429,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(1); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('should have accurate dry run', async () => { | ||||
| @ -451,8 +451,8 @@ describe(`immich upload`, () => { | ||||
|       ); | ||||
|       expect(exitCode).toBe(0); | ||||
| 
 | ||||
|       const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.length).toBe(0); | ||||
|       const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); | ||||
|       expect(assets.total).toBe(0); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -17,7 +17,6 @@ import { | ||||
|   createSharedLink, | ||||
|   createUser, | ||||
|   deleteAssets, | ||||
|   getAllAssets, | ||||
|   getAllJobsStatus, | ||||
|   getAssetInfo, | ||||
|   getConfigDefaults, | ||||
| @ -340,8 +339,6 @@ export const utils = { | ||||
| 
 | ||||
|   getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), | ||||
| 
 | ||||
|   getAllAssets: (accessToken: string) => getAllAssets({}, { headers: asBearerAuth(accessToken) }), | ||||
| 
 | ||||
|   metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => { | ||||
|     return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); | ||||
|   }, | ||||
|  | ||||
							
								
								
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @ -96,7 +96,6 @@ Class | Method | HTTP request | Description | ||||
| *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check |  | ||||
| *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |  | ||||
| *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset |  | ||||
| *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |  | ||||
| *AssetApi* | [**getAllUserAssetsByDeviceId**](doc//AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} |  | ||||
| *AssetApi* | [**getAssetInfo**](doc//AssetApi.md#getassetinfo) | **GET** /asset/{id} |  | ||||
| *AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics |  | ||||
|  | ||||
							
								
								
									
										111
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										111
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @ -159,117 +159,6 @@ class AssetApi { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Get all AssetEntity belong to the user | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] ifNoneMatch: | ||||
|   ///   ETag of data already cached on the client | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [int] skip: | ||||
|   /// | ||||
|   /// * [int] take: | ||||
|   /// | ||||
|   /// * [DateTime] updatedAfter: | ||||
|   /// | ||||
|   /// * [DateTime] updatedBefore: | ||||
|   /// | ||||
|   /// * [String] userId: | ||||
|   Future<Response> getAllAssetsWithHttpInfo({ String? ifNoneMatch, bool? isArchived, bool? isFavorite, int? skip, int? take, DateTime? updatedAfter, DateTime? updatedBefore, String? userId, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     if (isArchived != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isArchived', isArchived)); | ||||
|     } | ||||
|     if (isFavorite != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); | ||||
|     } | ||||
|     if (skip != null) { | ||||
|       queryParams.addAll(_queryParams('', 'skip', skip)); | ||||
|     } | ||||
|     if (take != null) { | ||||
|       queryParams.addAll(_queryParams('', 'take', take)); | ||||
|     } | ||||
|     if (updatedAfter != null) { | ||||
|       queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); | ||||
|     } | ||||
|     if (updatedBefore != null) { | ||||
|       queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore)); | ||||
|     } | ||||
|     if (userId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'userId', userId)); | ||||
|     } | ||||
| 
 | ||||
|     if (ifNoneMatch != null) { | ||||
|       headerParams[r'if-none-match'] = parameterToString(ifNoneMatch); | ||||
|     } | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'GET', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Get all AssetEntity belong to the user | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] ifNoneMatch: | ||||
|   ///   ETag of data already cached on the client | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [int] skip: | ||||
|   /// | ||||
|   /// * [int] take: | ||||
|   /// | ||||
|   /// * [DateTime] updatedAfter: | ||||
|   /// | ||||
|   /// * [DateTime] updatedBefore: | ||||
|   /// | ||||
|   /// * [String] userId: | ||||
|   Future<List<AssetResponseDto>?> getAllAssets({ String? ifNoneMatch, bool? isArchived, bool? isFavorite, int? skip, int? take, DateTime? updatedAfter, DateTime? updatedBefore, String? userId, }) async { | ||||
|     final response = await getAllAssetsWithHttpInfo( ifNoneMatch: ifNoneMatch, isArchived: isArchived, isFavorite: isFavorite, skip: skip, take: take, updatedAfter: updatedAfter, updatedBefore: updatedBefore, 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) { | ||||
|       final responseBody = await _decodeBodyBytes(response); | ||||
|       return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List) | ||||
|         .cast<AssetResponseDto>() | ||||
|         .toList(growable: false); | ||||
| 
 | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Get all asset of a device that are in the database, ID only. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|  | ||||
| @ -969,109 +969,6 @@ | ||||
|           "Asset" | ||||
|         ] | ||||
|       }, | ||||
|       "get": { | ||||
|         "description": "Get all AssetEntity belong to the user", | ||||
|         "operationId": "getAllAssets", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "if-none-match", | ||||
|             "in": "header", | ||||
|             "description": "ETag of data already cached on the client", | ||||
|             "required": false, | ||||
|             "schema": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isArchived", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isFavorite", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "skip", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "integer" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "take", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "integer" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "updatedAfter", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "format": "date-time", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "updatedBefore", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "format": "date-time", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "userId", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "items": { | ||||
|                     "$ref": "#/components/schemas/AssetResponseDto" | ||||
|                   }, | ||||
|                   "type": "array" | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Asset" | ||||
|         ] | ||||
|       }, | ||||
|       "put": { | ||||
|         "operationId": "updateAssets", | ||||
|         "parameters": [], | ||||
|  | ||||
| @ -13,15 +13,14 @@ npm i --save @immich/sdk | ||||
| For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli). | ||||
| 
 | ||||
| ```typescript | ||||
| import { getAllAlbums, getAllAssets, getMyUserInfo, init } from "@immich/sdk"; | ||||
| import { getAllAlbums, getMyUserInfo, init } from "@immich/sdk"; | ||||
| 
 | ||||
| const API_KEY = "<API_KEY>"; // process.env.IMMICH_API_KEY | ||||
| 
 | ||||
| init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY }); | ||||
| 
 | ||||
| const user = await getMyUserInfo(); | ||||
| const assets = await getAllAssets({ take: 1000 }); | ||||
| const albums = await getAllAlbums({}); | ||||
| 
 | ||||
| console.log({ user, assets, albums }); | ||||
| console.log({ user, albums }); | ||||
| ``` | ||||
|  | ||||
| @ -1338,37 +1338,6 @@ export function deleteAssets({ assetBulkDeleteDto }: { | ||||
|         body: assetBulkDeleteDto | ||||
|     }))); | ||||
| } | ||||
| /** | ||||
|  * Get all AssetEntity belong to the user | ||||
|  */ | ||||
| export function getAllAssets({ ifNoneMatch, isArchived, isFavorite, skip, take, updatedAfter, updatedBefore, userId }: { | ||||
|     ifNoneMatch?: string; | ||||
|     isArchived?: boolean; | ||||
|     isFavorite?: boolean; | ||||
|     skip?: number; | ||||
|     take?: number; | ||||
|     updatedAfter?: string; | ||||
|     updatedBefore?: string; | ||||
|     userId?: string; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchJson<{ | ||||
|         status: 200; | ||||
|         data: AssetResponseDto[]; | ||||
|     }>(`/asset${QS.query(QS.explode({ | ||||
|         isArchived, | ||||
|         isFavorite, | ||||
|         skip, | ||||
|         take, | ||||
|         updatedAfter, | ||||
|         updatedBefore, | ||||
|         userId | ||||
|     }))}`, {
 | ||||
|         ...opts, | ||||
|         headers: oazapfts.mergeHeaders(opts?.headers, { | ||||
|             "if-none-match": ifNoneMatch | ||||
|         }) | ||||
|     })); | ||||
| } | ||||
| export function updateAssets({ assetBulkUpdateDto }: { | ||||
|     assetBulkUpdateDto: AssetBulkUpdateDto; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|  | ||||
| @ -1,10 +1,12 @@ | ||||
| import { | ||||
|   Body, | ||||
|   Controller, | ||||
|   HttpCode, | ||||
|   HttpStatus, | ||||
|   Inject, | ||||
|   Param, | ||||
|   ParseFilePipe, | ||||
|   Post, | ||||
|   Put, | ||||
|   Res, | ||||
|   UploadedFiles, | ||||
| @ -13,8 +15,18 @@ import { | ||||
| import { ApiConsumes, ApiTags } from '@nestjs/swagger'; | ||||
| import { Response } from 'express'; | ||||
| import { EndpointLifecycle } from 'src/decorators'; | ||||
| import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto'; | ||||
| import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckResponseDto, | ||||
|   AssetMediaResponseDto, | ||||
|   AssetMediaStatusEnum, | ||||
|   CheckExistingAssetsResponseDto, | ||||
| } from 'src/dtos/asset-media-response.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckDto, | ||||
|   AssetMediaReplaceDto, | ||||
|   CheckExistingAssetsDto, | ||||
|   UploadFieldName, | ||||
| } from 'src/dtos/asset-media.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||
| import { Auth, Authenticated } from 'src/middleware/auth.guard'; | ||||
| @ -53,4 +65,30 @@ export class AssetMediaController { | ||||
|     } | ||||
|     return responseDto; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|    */ | ||||
|   @Post('exist') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   @Authenticated() | ||||
|   checkExistingAssets( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Body() dto: CheckExistingAssetsDto, | ||||
|   ): Promise<CheckExistingAssetsResponseDto> { | ||||
|     return this.service.checkExistingAssets(auth, dto); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Checks if assets exist by checksums | ||||
|    */ | ||||
|   @Post('bulk-upload-check') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   @Authenticated() | ||||
|   checkBulkUpload( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Body() dto: AssetBulkUploadCheckDto, | ||||
|   ): Promise<AssetBulkUploadCheckResponseDto> { | ||||
|     return this.service.bulkUploadCheck(auth, dto); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,6 @@ import { | ||||
|   Body, | ||||
|   Controller, | ||||
|   Get, | ||||
|   HttpCode, | ||||
|   HttpStatus, | ||||
|   Inject, | ||||
|   Next, | ||||
| @ -16,20 +15,8 @@ import { | ||||
| } from '@nestjs/common'; | ||||
| import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; | ||||
| import { NextFunction, Response } from 'express'; | ||||
| import { AssetResponseDto } from 'src/dtos/asset-response.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckResponseDto, | ||||
|   AssetFileUploadResponseDto, | ||||
|   CheckExistingAssetsResponseDto, | ||||
| } from 'src/dtos/asset-v1-response.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckDto, | ||||
|   AssetSearchDto, | ||||
|   CheckExistingAssetsDto, | ||||
|   CreateAssetDto, | ||||
|   GetAssetThumbnailDto, | ||||
|   ServeFileDto, | ||||
| } from 'src/dtos/asset-v1.dto'; | ||||
| import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto'; | ||||
| import { CreateAssetDto, GetAssetThumbnailDto, ServeFileDto } from 'src/dtos/asset-v1.dto'; | ||||
| import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto'; | ||||
| import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||
| import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; | ||||
| @ -109,45 +96,4 @@ export class AssetControllerV1 { | ||||
|   ) { | ||||
|     await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto), this.logger); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get all AssetEntity belong to the user | ||||
|    */ | ||||
|   @Get('/') | ||||
|   @ApiHeader({ | ||||
|     name: 'if-none-match', | ||||
|     description: 'ETag of data already cached on the client', | ||||
|     required: false, | ||||
|     schema: { type: 'string' }, | ||||
|   }) | ||||
|   @Authenticated() | ||||
|   getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> { | ||||
|     return this.service.getAllAssets(auth, dto); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Checks if multiple assets exist on the server and returns all existing - used by background backup | ||||
|    */ | ||||
|   @Post('/exist') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   @Authenticated() | ||||
|   checkExistingAssets( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Body() dto: CheckExistingAssetsDto, | ||||
|   ): Promise<CheckExistingAssetsResponseDto> { | ||||
|     return this.service.checkExistingAssets(auth, dto); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Checks if assets exist by checksums | ||||
|    */ | ||||
|   @Post('/bulk-upload-check') | ||||
|   @HttpCode(HttpStatus.OK) | ||||
|   @Authenticated() | ||||
|   checkBulkUpload( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Body() dto: AssetBulkUploadCheckDto, | ||||
|   ): Promise<AssetBulkUploadCheckResponseDto> { | ||||
|     return this.service.bulkUploadCheck(auth, dto); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -9,3 +9,28 @@ export class AssetMediaResponseDto { | ||||
|   status!: AssetMediaStatusEnum; | ||||
|   id!: string; | ||||
| } | ||||
| 
 | ||||
| export enum AssetUploadAction { | ||||
|   ACCEPT = 'accept', | ||||
|   REJECT = 'reject', | ||||
| } | ||||
| 
 | ||||
| export enum AssetRejectReason { | ||||
|   DUPLICATE = 'duplicate', | ||||
|   UNSUPPORTED_FORMAT = 'unsupported-format', | ||||
| } | ||||
| 
 | ||||
| export class AssetBulkUploadCheckResult { | ||||
|   id!: string; | ||||
|   action!: AssetUploadAction; | ||||
|   reason?: AssetRejectReason; | ||||
|   assetId?: string; | ||||
| } | ||||
| 
 | ||||
| export class AssetBulkUploadCheckResponseDto { | ||||
|   results!: AssetBulkUploadCheckResult[]; | ||||
| } | ||||
| 
 | ||||
| export class CheckExistingAssetsResponseDto { | ||||
|   existingIds!: string[]; | ||||
| } | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; | ||||
| import { Optional, ValidateDate } from 'src/validation'; | ||||
| 
 | ||||
| export enum UploadFieldName { | ||||
| @ -33,3 +34,31 @@ export class AssetMediaReplaceDto { | ||||
|   @ApiProperty({ type: 'string', format: 'binary' }) | ||||
|   [UploadFieldName.ASSET_DATA]!: any; | ||||
| } | ||||
| 
 | ||||
| export class AssetBulkUploadCheckItem { | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   id!: string; | ||||
| 
 | ||||
|   /** base64 or hex encoded sha1 hash */ | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   checksum!: string; | ||||
| } | ||||
| 
 | ||||
| export class AssetBulkUploadCheckDto { | ||||
|   @IsArray() | ||||
|   @ValidateNested({ each: true }) | ||||
|   @Type(() => AssetBulkUploadCheckItem) | ||||
|   assets!: AssetBulkUploadCheckItem[]; | ||||
| } | ||||
| 
 | ||||
| export class CheckExistingAssetsDto { | ||||
|   @ArrayNotEmpty() | ||||
|   @IsString({ each: true }) | ||||
|   @IsNotEmpty({ each: true }) | ||||
|   deviceAssetIds!: string[]; | ||||
| 
 | ||||
|   @IsNotEmpty() | ||||
|   deviceId!: string; | ||||
| } | ||||
|  | ||||
| @ -1,29 +1,4 @@ | ||||
| export class AssetBulkUploadCheckResult { | ||||
|   id!: string; | ||||
|   action!: AssetUploadAction; | ||||
|   reason?: AssetRejectReason; | ||||
|   assetId?: string; | ||||
| } | ||||
| 
 | ||||
| export class AssetBulkUploadCheckResponseDto { | ||||
|   results!: AssetBulkUploadCheckResult[]; | ||||
| } | ||||
| 
 | ||||
| export enum AssetUploadAction { | ||||
|   ACCEPT = 'accept', | ||||
|   REJECT = 'reject', | ||||
| } | ||||
| 
 | ||||
| export enum AssetRejectReason { | ||||
|   DUPLICATE = 'duplicate', | ||||
|   UNSUPPORTED_FORMAT = 'unsupported-format', | ||||
| } | ||||
| 
 | ||||
| export class AssetFileUploadResponseDto { | ||||
|   id!: string; | ||||
|   duplicate!: boolean; | ||||
| } | ||||
| 
 | ||||
| export class CheckExistingAssetsResponseDto { | ||||
|   existingIds!: string[]; | ||||
| } | ||||
|  | ||||
| @ -1,68 +1,8 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator'; | ||||
| import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; | ||||
| import { UploadFieldName } from 'src/dtos/asset.dto'; | ||||
| import { Optional, ValidateBoolean, ValidateDate } from 'src/validation'; | ||||
| 
 | ||||
| export class AssetBulkUploadCheckItem { | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   id!: string; | ||||
| 
 | ||||
|   /** base64 or hex encoded sha1 hash */ | ||||
|   @IsString() | ||||
|   @IsNotEmpty() | ||||
|   checksum!: string; | ||||
| } | ||||
| 
 | ||||
| export class AssetBulkUploadCheckDto { | ||||
|   @IsArray() | ||||
|   @ValidateNested({ each: true }) | ||||
|   @Type(() => AssetBulkUploadCheckItem) | ||||
|   assets!: AssetBulkUploadCheckItem[]; | ||||
| } | ||||
| 
 | ||||
| export class AssetSearchDto { | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isFavorite?: boolean; | ||||
| 
 | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   isArchived?: boolean; | ||||
| 
 | ||||
|   @Optional() | ||||
|   @IsInt() | ||||
|   @Type(() => Number) | ||||
|   @ApiProperty({ type: 'integer' }) | ||||
|   skip?: number; | ||||
| 
 | ||||
|   @Optional() | ||||
|   @IsInt() | ||||
|   @Type(() => Number) | ||||
|   @ApiProperty({ type: 'integer' }) | ||||
|   take?: number; | ||||
| 
 | ||||
|   @Optional() | ||||
|   @IsUUID('4') | ||||
|   @ApiProperty({ format: 'uuid' }) | ||||
|   userId?: string; | ||||
| 
 | ||||
|   @ValidateDate({ optional: true }) | ||||
|   updatedAfter?: Date; | ||||
| 
 | ||||
|   @ValidateDate({ optional: true }) | ||||
|   updatedBefore?: Date; | ||||
| } | ||||
| 
 | ||||
| export class CheckExistingAssetsDto { | ||||
|   @ArrayNotEmpty() | ||||
|   @IsString({ each: true }) | ||||
|   @IsNotEmpty({ each: true }) | ||||
|   deviceAssetIds!: string[]; | ||||
| 
 | ||||
|   @IsNotEmpty() | ||||
|   deviceId!: string; | ||||
| } | ||||
| 
 | ||||
| export class CreateAssetDto { | ||||
|   @IsNotEmpty() | ||||
|   @IsString() | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto'; | ||||
| import { AssetEntity } from 'src/entities/asset.entity'; | ||||
| 
 | ||||
| export interface AssetCheck { | ||||
| @ -12,10 +11,7 @@ export interface AssetOwnerCheck extends AssetCheck { | ||||
| 
 | ||||
| export interface IAssetRepositoryV1 { | ||||
|   get(id: string): Promise<AssetEntity | null>; | ||||
|   getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>; | ||||
|   getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>; | ||||
|   getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>; | ||||
|   getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>; | ||||
| } | ||||
| 
 | ||||
| export const IAssetRepositoryV1 = 'IAssetRepositoryV1'; | ||||
|  | ||||
| @ -168,8 +168,10 @@ export interface IAssetRepository { | ||||
|   getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>; | ||||
|   getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>; | ||||
|   getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null>; | ||||
|   getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>; | ||||
|   getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>; | ||||
|   getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>; | ||||
|   getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]>; | ||||
|   getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>; | ||||
|   getById( | ||||
|     id: string, | ||||
|  | ||||
| @ -482,6 +482,20 @@ WHERE | ||||
| LIMIT | ||||
|   1 | ||||
| 
 | ||||
| -- AssetRepository.getByChecksums | ||||
| SELECT | ||||
|   "AssetEntity"."id" AS "AssetEntity_id", | ||||
|   "AssetEntity"."checksum" AS "AssetEntity_checksum" | ||||
| FROM | ||||
|   "assets" "AssetEntity" | ||||
| WHERE | ||||
|   ( | ||||
|     ("AssetEntity"."ownerId" = $1) | ||||
|     AND ( | ||||
|       "AssetEntity"."checksum" IN ($2, $3, $4, $5, $6, $7, $8, $9, $10) | ||||
|     ) | ||||
|   ) | ||||
| 
 | ||||
| -- AssetRepository.getUploadAssetIdByChecksum | ||||
| SELECT | ||||
|   "AssetEntity"."id" AS "AssetEntity_id" | ||||
|  | ||||
| @ -74,43 +74,6 @@ WHERE | ||||
|   ((("LibraryEntity"."ownerId" = $1))) | ||||
|   AND ("LibraryEntity"."deletedAt" IS NULL) | ||||
| 
 | ||||
| -- LibraryRepository.getAllByUserId | ||||
| SELECT | ||||
|   "LibraryEntity"."id" AS "LibraryEntity_id", | ||||
|   "LibraryEntity"."name" AS "LibraryEntity_name", | ||||
|   "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", | ||||
|   "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", | ||||
|   "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", | ||||
|   "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", | ||||
|   "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", | ||||
|   "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", | ||||
|   "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", | ||||
|   "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", | ||||
|   "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", | ||||
|   "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", | ||||
|   "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", | ||||
|   "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", | ||||
|   "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", | ||||
|   "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", | ||||
|   "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", | ||||
|   "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", | ||||
|   "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", | ||||
|   "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", | ||||
|   "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", | ||||
|   "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", | ||||
|   "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" | ||||
| FROM | ||||
|   "libraries" "LibraryEntity" | ||||
|   LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" | ||||
|   AND ( | ||||
|     "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL | ||||
|   ) | ||||
| WHERE | ||||
|   ((("LibraryEntity"."ownerId" = $1))) | ||||
|   AND ("LibraryEntity"."deletedAt" IS NULL) | ||||
| ORDER BY | ||||
|   "LibraryEntity"."createdAt" ASC | ||||
| 
 | ||||
| -- LibraryRepository.getAll | ||||
| SELECT | ||||
|   "LibraryEntity"."id" AS "LibraryEntity_id", | ||||
|  | ||||
| @ -1,9 +1,7 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto'; | ||||
| import { AssetEntity } from 'src/entities/asset.entity'; | ||||
| import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; | ||||
| import { OptionalBetween } from 'src/utils/database'; | ||||
| import { AssetCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface'; | ||||
| import { In } from 'typeorm/find-options/operator/In.js'; | ||||
| import { Repository } from 'typeorm/repository/Repository.js'; | ||||
| 
 | ||||
| @ -11,36 +9,6 @@ import { Repository } from 'typeorm/repository/Repository.js'; | ||||
| export class AssetRepositoryV1 implements IAssetRepositoryV1 { | ||||
|   constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {} | ||||
| 
 | ||||
|   /** | ||||
|    * Retrieves all assets by user ID. | ||||
|    * | ||||
|    * @param ownerId - The ID of the owner. | ||||
|    * @param dto - The AssetSearchDto object containing search criteria. | ||||
|    * @returns A Promise that resolves to an array of AssetEntity objects. | ||||
|    */ | ||||
|   getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> { | ||||
|     return this.assetRepository.find({ | ||||
|       where: { | ||||
|         ownerId, | ||||
|         isVisible: true, | ||||
|         isFavorite: dto.isFavorite, | ||||
|         isArchived: dto.isArchived, | ||||
|         updatedAt: OptionalBetween(dto.updatedAfter, dto.updatedBefore), | ||||
|       }, | ||||
|       relations: { | ||||
|         exifInfo: true, | ||||
|         tags: true, | ||||
|         stack: { assets: true }, | ||||
|       }, | ||||
|       skip: dto.skip || 0, | ||||
|       take: dto.take, | ||||
|       order: { | ||||
|         fileCreatedAt: 'DESC', | ||||
|       }, | ||||
|       withDeleted: true, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   get(id: string): Promise<AssetEntity | null> { | ||||
|     return this.assetRepository.findOne({ | ||||
|       where: { id }, | ||||
| @ -73,30 +41,4 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { | ||||
|       withDeleted: true, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]> { | ||||
|     const assets = await this.assetRepository.find({ | ||||
|       select: { deviceAssetId: true }, | ||||
|       where: { | ||||
|         deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds), | ||||
|         deviceId: checkDuplicateAssetDto.deviceId, | ||||
|         ownerId, | ||||
|       }, | ||||
|       withDeleted: true, | ||||
|     }); | ||||
|     return assets.map((asset) => asset.deviceAssetId); | ||||
|   } | ||||
| 
 | ||||
|   getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> { | ||||
|     return this.assetRepository.findOne({ | ||||
|       select: { | ||||
|         id: true, | ||||
|         ownerId: true, | ||||
|         checksum: true, | ||||
|       }, | ||||
|       where: { | ||||
|         originalPath, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -157,6 +157,18 @@ export class AssetRepository implements IAssetRepository { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]> { | ||||
|     return this.repository.find({ | ||||
|       select: { deviceAssetId: true }, | ||||
|       where: { | ||||
|         deviceAssetId: In(deviceAssetIds), | ||||
|         deviceId, | ||||
|         ownerId, | ||||
|       }, | ||||
|       withDeleted: true, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   getByUserId( | ||||
|     pagination: PaginationOptions, | ||||
|     userId: string, | ||||
| @ -300,6 +312,21 @@ export class AssetRepository implements IAssetRepository { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) | ||||
|   getByChecksums(ownerId: string, checksums: Buffer[]): Promise<AssetEntity[]> { | ||||
|     return this.repository.find({ | ||||
|       select: { | ||||
|         id: true, | ||||
|         checksum: true, | ||||
|       }, | ||||
|       where: { | ||||
|         ownerId, | ||||
|         checksum: In(checksums), | ||||
|       }, | ||||
|       withDeleted: true, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID, DummyValue.BUFFER] }) | ||||
|   async getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined> { | ||||
|     const asset = await this.repository.findOne({ | ||||
|  | ||||
| @ -39,21 +39,6 @@ export class LibraryRepository implements ILibraryRepository { | ||||
|     return this.repository.countBy({ ownerId }); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   getAllByUserId(ownerId: string): Promise<LibraryEntity[]> { | ||||
|     return this.repository.find({ | ||||
|       where: { | ||||
|         ownerId, | ||||
|       }, | ||||
|       relations: { | ||||
|         owner: true, | ||||
|       }, | ||||
|       order: { | ||||
|         createdAt: 'ASC', | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   @GenerateSql({ params: [] }) | ||||
|   getAll(withDeleted = false): Promise<LibraryEntity[]> { | ||||
|     return this.repository.find({ | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Stats } from 'node:fs'; | ||||
| import { AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto'; | ||||
| import { AssetMediaStatusEnum, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; | ||||
| import { AssetMediaReplaceDto } from 'src/dtos/asset-media.dto'; | ||||
| import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; | ||||
| import { ExifEntity } from 'src/entities/exif.entity'; | ||||
| @ -277,4 +277,31 @@ describe('AssetMediaService', () => { | ||||
|       expect(userMock.updateUsage).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|   describe('bulkUploadCheck', () => { | ||||
|     it('should accept hex and base64 checksums', async () => { | ||||
|       const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); | ||||
|       const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); | ||||
| 
 | ||||
|       assetMock.getByChecksums.mockResolvedValue([ | ||||
|         { id: 'asset-1', checksum: file1 } as AssetEntity, | ||||
|         { id: 'asset-2', checksum: file2 } as AssetEntity, | ||||
|       ]); | ||||
| 
 | ||||
|       await expect( | ||||
|         sut.bulkUploadCheck(authStub.admin, { | ||||
|           assets: [ | ||||
|             { id: '1', checksum: file1.toString('hex') }, | ||||
|             { id: '2', checksum: file2.toString('base64') }, | ||||
|           ], | ||||
|         }), | ||||
|       ).resolves.toEqual({ | ||||
|         results: [ | ||||
|           { id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, | ||||
|           { id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, | ||||
|         ], | ||||
|       }); | ||||
| 
 | ||||
|       expect(assetMock.getByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -1,7 +1,19 @@ | ||||
| import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; | ||||
| import { AccessCore, Permission } from 'src/cores/access.core'; | ||||
| import { AssetMediaResponseDto, AssetMediaStatusEnum } from 'src/dtos/asset-media-response.dto'; | ||||
| import { AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckResponseDto, | ||||
|   AssetMediaResponseDto, | ||||
|   AssetMediaStatusEnum, | ||||
|   AssetRejectReason, | ||||
|   AssetUploadAction, | ||||
|   CheckExistingAssetsResponseDto, | ||||
| } from 'src/dtos/asset-media-response.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckDto, | ||||
|   AssetMediaReplaceDto, | ||||
|   CheckExistingAssetsDto, | ||||
|   UploadFieldName, | ||||
| } from 'src/dtos/asset-media.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; | ||||
| import { IAccessRepository } from 'src/interfaces/access.interface'; | ||||
| @ -12,8 +24,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||
| import { IStorageRepository } from 'src/interfaces/storage.interface'; | ||||
| import { IUserRepository } from 'src/interfaces/user.interface'; | ||||
| import { mimeTypes } from 'src/utils/mime-types'; | ||||
| import { fromChecksum } from 'src/utils/request'; | ||||
| import { QueryFailedError } from 'typeorm'; | ||||
| 
 | ||||
| export interface UploadRequest { | ||||
|   auth: AuthDto | null; | ||||
|   fieldName: UploadFieldName; | ||||
| @ -174,4 +186,49 @@ export class AssetMediaService { | ||||
|       throw new BadRequestException('Quota has been exceeded!'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async checkExistingAssets( | ||||
|     auth: AuthDto, | ||||
|     checkExistingAssetsDto: CheckExistingAssetsDto, | ||||
|   ): Promise<CheckExistingAssetsResponseDto> { | ||||
|     const assets = await this.assetRepository.getByDeviceIds( | ||||
|       auth.user.id, | ||||
|       checkExistingAssetsDto.deviceId, | ||||
|       checkExistingAssetsDto.deviceAssetIds, | ||||
|     ); | ||||
|     return { | ||||
|       existingIds: assets.map((asset) => asset.id), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> { | ||||
|     const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); | ||||
|     const results = await this.assetRepository.getByChecksums(auth.user.id, checksums); | ||||
|     const checksumMap: Record<string, string> = {}; | ||||
| 
 | ||||
|     for (const { id, checksum } of results) { | ||||
|       checksumMap[checksum.toString('hex')] = id; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       results: dto.assets.map(({ id, checksum }) => { | ||||
|         const duplicate = checksumMap[fromChecksum(checksum).toString('hex')]; | ||||
|         if (duplicate) { | ||||
|           return { | ||||
|             id, | ||||
|             assetId: duplicate, | ||||
|             action: AssetUploadAction.REJECT, | ||||
|             reason: AssetRejectReason.DUPLICATE, | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         // TODO mime-check
 | ||||
| 
 | ||||
|         return { | ||||
|           id, | ||||
|           action: AssetUploadAction.ACCEPT, | ||||
|         }; | ||||
|       }), | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,3 @@ | ||||
| import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto'; | ||||
| import { CreateAssetDto } from 'src/dtos/asset-v1.dto'; | ||||
| import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; | ||||
| import { ExifEntity } from 'src/entities/exif.entity'; | ||||
| @ -74,10 +73,7 @@ describe('AssetService', () => { | ||||
|   beforeEach(() => { | ||||
|     assetRepositoryMockV1 = { | ||||
|       get: vitest.fn(), | ||||
|       getAllByUserId: vitest.fn(), | ||||
|       getAssetsByChecksums: vitest.fn(), | ||||
|       getExistingAssets: vitest.fn(), | ||||
|       getByOriginalPath: vitest.fn(), | ||||
|     }; | ||||
| 
 | ||||
|     accessMock = newAccessRepositoryMock(); | ||||
| @ -194,32 +190,4 @@ describe('AssetService', () => { | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('bulkUploadCheck', () => { | ||||
|     it('should accept hex and base64 checksums', async () => { | ||||
|       const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex'); | ||||
|       const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex'); | ||||
| 
 | ||||
|       assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([ | ||||
|         { id: 'asset-1', checksum: file1 }, | ||||
|         { id: 'asset-2', checksum: file2 }, | ||||
|       ]); | ||||
| 
 | ||||
|       await expect( | ||||
|         sut.bulkUploadCheck(authStub.admin, { | ||||
|           assets: [ | ||||
|             { id: '1', checksum: file1.toString('hex') }, | ||||
|             { id: '2', checksum: file2.toString('base64') }, | ||||
|           ], | ||||
|         }), | ||||
|       ).resolves.toEqual({ | ||||
|         results: [ | ||||
|           { id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, | ||||
|           { id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, | ||||
|         ], | ||||
|       }); | ||||
| 
 | ||||
|       expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -6,23 +6,8 @@ import { | ||||
|   NotFoundException, | ||||
| } from '@nestjs/common'; | ||||
| import { AccessCore, Permission } from 'src/cores/access.core'; | ||||
| import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckResponseDto, | ||||
|   AssetFileUploadResponseDto, | ||||
|   AssetRejectReason, | ||||
|   AssetUploadAction, | ||||
|   CheckExistingAssetsResponseDto, | ||||
| } from 'src/dtos/asset-v1-response.dto'; | ||||
| import { | ||||
|   AssetBulkUploadCheckDto, | ||||
|   AssetSearchDto, | ||||
|   CheckExistingAssetsDto, | ||||
|   CreateAssetDto, | ||||
|   GetAssetThumbnailDto, | ||||
|   GetAssetThumbnailFormatEnum, | ||||
|   ServeFileDto, | ||||
| } from 'src/dtos/asset-v1.dto'; | ||||
| import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto'; | ||||
| import { CreateAssetDto, GetAssetThumbnailDto, GetAssetThumbnailFormatEnum, ServeFileDto } from 'src/dtos/asset-v1.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; | ||||
| import { IAccessRepository } from 'src/interfaces/access.interface'; | ||||
| @ -36,7 +21,6 @@ import { IUserRepository } from 'src/interfaces/user.interface'; | ||||
| import { UploadFile } from 'src/services/asset-media.service'; | ||||
| import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file'; | ||||
| import { mimeTypes } from 'src/utils/mime-types'; | ||||
| import { fromChecksum } from 'src/utils/request'; | ||||
| import { QueryFailedError } from 'typeorm'; | ||||
| 
 | ||||
| @Injectable() | ||||
| @ -112,13 +96,6 @@ export class AssetServiceV1 { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> { | ||||
|     const userId = dto.userId || auth.user.id; | ||||
|     await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); | ||||
|     const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto); | ||||
|     return assets.map((asset) => mapAsset(asset, { withStack: true, auth })); | ||||
|   } | ||||
| 
 | ||||
|   async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> { | ||||
|     await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId); | ||||
| 
 | ||||
| @ -159,46 +136,6 @@ export class AssetServiceV1 { | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async checkExistingAssets( | ||||
|     auth: AuthDto, | ||||
|     checkExistingAssetsDto: CheckExistingAssetsDto, | ||||
|   ): Promise<CheckExistingAssetsResponseDto> { | ||||
|     return { | ||||
|       existingIds: await this.assetRepositoryV1.getExistingAssets(auth.user.id, checkExistingAssetsDto), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> { | ||||
|     const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); | ||||
|     const results = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums); | ||||
|     const checksumMap: Record<string, string> = {}; | ||||
| 
 | ||||
|     for (const { id, checksum } of results) { | ||||
|       checksumMap[checksum.toString('hex')] = id; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       results: dto.assets.map(({ id, checksum }) => { | ||||
|         const duplicate = checksumMap[fromChecksum(checksum).toString('hex')]; | ||||
|         if (duplicate) { | ||||
|           return { | ||||
|             id, | ||||
|             assetId: duplicate, | ||||
|             action: AssetUploadAction.REJECT, | ||||
|             reason: AssetRejectReason.DUPLICATE, | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         // TODO mime-check
 | ||||
| 
 | ||||
|         return { | ||||
|           id, | ||||
|           action: AssetUploadAction.ACCEPT, | ||||
|         }; | ||||
|       }), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { | ||||
|     switch (format) { | ||||
|       case GetAssetThumbnailFormatEnum.WEBP: { | ||||
|  | ||||
| @ -10,10 +10,12 @@ export const newAssetRepositoryMock = (): Mocked<IAssetRepository> => { | ||||
|     getByIds: vitest.fn().mockResolvedValue([]), | ||||
|     getByIdsWithAllRelations: vitest.fn().mockResolvedValue([]), | ||||
|     getByAlbumId: vitest.fn(), | ||||
|     getByDeviceIds: vitest.fn(), | ||||
|     getByUserId: vitest.fn(), | ||||
|     getById: vitest.fn(), | ||||
|     getWithout: vitest.fn(), | ||||
|     getByChecksum: vitest.fn(), | ||||
|     getByChecksums: vitest.fn(), | ||||
|     getUploadAssetIdByChecksum: vitest.fn(), | ||||
|     getWith: vitest.fn(), | ||||
|     getRandom: vitest.fn(), | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user