1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

fix(server): getAllAssets doesn't return all assets (#7752)

* fix(server): getAllAssets doesn't return all assets

* try reverting

* fix: archive and remove unused method

* update sql

* remove unused code

* linting
This commit is contained in:
Alex 2024-03-08 17:16:32 -06:00 committed by GitHub
parent 7a4ae7d142
commit e8fb529026
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 42 additions and 100 deletions

View File

@ -1,9 +1,4 @@
import { import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain';
AssetSearchOneToOneRelationOptions,
AssetSearchOptions,
ReverseGeocodeResult,
SearchExploreItem,
} from '@app/domain';
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
import { Paginated, PaginationOptions } from '../domain.util'; import { Paginated, PaginationOptions } from '../domain.util';
@ -140,10 +135,6 @@ export interface IAssetRepository {
updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise<void>; updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise<void>;
deleteAll(ownerId: string): Promise<void>; deleteAll(ownerId: string): Promise<void>;
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
getAllByFileCreationDate(
pagination: PaginationOptions,
options?: AssetSearchOneToOneRelationOptions,
): Paginated<AssetEntity>;
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>; getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>; updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>;
save(asset: Pick<AssetEntity, 'id'> & Partial<AssetEntity>): Promise<AssetEntity>; save(asset: Pick<AssetEntity, 'id'> & Partial<AssetEntity>): Promise<AssetEntity>;

View File

@ -1,13 +1,14 @@
import { AssetEntity } from '@app/infra/entities'; import { AssetEntity, ExifEntity } from '@app/infra/entities';
import { OptionalBetween } from '@app/infra/infra.utils';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { In } from 'typeorm/find-options/operator/In.js'; import { In } from 'typeorm/find-options/operator/In.js';
import { Repository } from 'typeorm/repository/Repository.js'; import { Repository } from 'typeorm/repository/Repository.js';
import { AssetSearchDto } from './dto/asset-search.dto';
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
import { SearchPropertiesDto } from './dto/search-properties.dto'; import { SearchPropertiesDto } from './dto/search-properties.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
export interface AssetCheck { export interface AssetCheck {
id: string; id: string;
checksum: Buffer; checksum: Buffer;
@ -21,6 +22,7 @@ export interface IAssetRepositoryV1 {
get(id: string): Promise<AssetEntity | null>; get(id: string): Promise<AssetEntity | null>;
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>; getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>; getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>; getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>; getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>; getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
@ -31,7 +33,40 @@ export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
@Injectable() @Injectable()
export class AssetRepositoryV1 implements IAssetRepositoryV1 { export class AssetRepositoryV1 implements IAssetRepositoryV1 {
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {} constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
) {}
/**
* 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,
});
}
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> { getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
return this.assetRepository return this.assetRepository

View File

@ -77,6 +77,7 @@ describe('AssetService', () => {
beforeEach(() => { beforeEach(() => {
assetRepositoryMockV1 = { assetRepositoryMockV1 = {
get: jest.fn(), get: jest.fn(),
getAllByUserId: jest.fn(),
getDetectedObjectsByUserId: jest.fn(), getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(), getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(), getSearchPropertiesByUserId: jest.fn(),

View File

@ -113,19 +113,8 @@ export class AssetService {
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> { public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
const userId = dto.userId || auth.user.id; const userId = dto.userId || auth.user.id;
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this.assetRepository.getAllByFileCreationDate( const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
{ take: dto.take ?? 1000, skip: dto.skip }, return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
{
...dto,
userIds: [userId],
withDeleted: true,
orderDirection: 'DESC',
withExif: true,
isVisible: true,
withStacked: true,
},
);
return assets.items.map((asset) => mapAsset(asset, { withStack: true }));
} }
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> { async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {

View File

@ -2,7 +2,6 @@ import {
AssetBuilderOptions, AssetBuilderOptions,
AssetCreate, AssetCreate,
AssetExploreFieldOptions, AssetExploreFieldOptions,
AssetSearchOneToOneRelationOptions,
AssetSearchOptions, AssetSearchOptions,
AssetStats, AssetStats,
AssetStatsOptions, AssetStatsOptions,
@ -233,29 +232,6 @@ export class AssetRepository implements IAssetRepository {
}); });
} }
@GenerateSql({
params: [
{ skip: 20_000, take: 10_000 },
{
takenBefore: DummyValue.DATE,
userIds: [DummyValue.UUID],
},
],
})
getAllByFileCreationDate(
pagination: PaginationOptions,
options: AssetSearchOneToOneRelationOptions = {},
): Paginated<AssetEntity> {
let builder = this.repository.createQueryBuilder('asset');
builder = searchAssetBuilder(builder, options);
builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC');
return paginatedBuilder<AssetEntity>(builder, {
mode: PaginationMode.LIMIT_OFFSET,
skip: pagination.skip,
take: pagination.take,
});
}
/** /**
* Get assets by device's Id on the database * Get assets by device's Id on the database
* @param ownerId * @param ownerId

View File

@ -428,55 +428,6 @@ WHERE
AND "isOffline" = $4 AND "isOffline" = $4
) )
-- AssetRepository.getAllByFileCreationDate
SELECT
"asset"."id" AS "asset_id",
"asset"."deviceAssetId" AS "asset_deviceAssetId",
"asset"."ownerId" AS "asset_ownerId",
"asset"."libraryId" AS "asset_libraryId",
"asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath",
"asset"."webpPath" AS "asset_webpPath",
"asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt",
"asset"."updatedAt" AS "asset_updatedAt",
"asset"."deletedAt" AS "asset_deletedAt",
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
"asset"."localDateTime" AS "asset_localDateTime",
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
"asset"."isFavorite" AS "asset_isFavorite",
"asset"."isArchived" AS "asset_isArchived",
"asset"."isExternal" AS "asset_isExternal",
"asset"."isReadOnly" AS "asset_isReadOnly",
"asset"."isOffline" AS "asset_isOffline",
"asset"."checksum" AS "asset_checksum",
"asset"."duration" AS "asset_duration",
"asset"."isVisible" AS "asset_isVisible",
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
"asset"."originalFileName" AS "asset_originalFileName",
"asset"."sidecarPath" AS "asset_sidecarPath",
"asset"."stackId" AS "asset_stackId"
FROM
"assets" "asset"
WHERE
(
"asset"."fileCreatedAt" <= $1
AND 1 = 1
AND "asset"."ownerId" IN ($2)
AND 1 = 1
AND "asset"."isArchived" = $3
)
AND ("asset"."deletedAt" IS NULL)
ORDER BY
"asset"."fileCreatedAt" DESC
LIMIT
10001
OFFSET
20000
-- AssetRepository.getAllByDeviceId -- AssetRepository.getAllByDeviceId
SELECT SELECT
"AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId",

View File

@ -18,7 +18,6 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
getFirstAssetForAlbumId: jest.fn(), getFirstAssetForAlbumId: jest.fn(),
getLastUpdatedAssetForAlbumId: jest.fn(), getLastUpdatedAssetForAlbumId: jest.fn(),
getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }), getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
getAllByFileCreationDate: jest.fn(),
getAllByDeviceId: jest.fn(), getAllByDeviceId: jest.fn(),
updateAll: jest.fn(), updateAll: jest.fn(),
getByLibraryId: jest.fn(), getByLibraryId: jest.fn(),