diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 07cf01f87f..5fe04860e5 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -482,7 +482,6 @@ Class | Method | HTTP request | Description - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryResponseDto](doc//MemoryResponseDto.md) - [MemorySearchOrder](doc//MemorySearchOrder.md) - - [MemorySearchResponseDto](doc//MemorySearchResponseDto.md) - [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md) - [MemoryType](doc//MemoryType.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 19e334aa0f..413a64040c 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -203,7 +203,6 @@ part 'model/memories_update.dart'; part 'model/memory_create_dto.dart'; part 'model/memory_response_dto.dart'; part 'model/memory_search_order.dart'; -part 'model/memory_search_response_dto.dart'; part 'model/memory_statistics_response_dto.dart'; part 'model/memory_type.dart'; part 'model/memory_update_dto.dart'; diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index c07ed32198..a36a7c83e7 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -522,7 +522,7 @@ class MemoriesApi { /// Number of memories to return /// /// * [MemoryType] type: - Future searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? page, int? size, MemoryType? type, Future? abortTrigger, }) async { + Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? page, int? size, MemoryType? type, Future? abortTrigger, }) async { final response = await searchMemoriesWithHttpInfo(for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, page: page, size: size, type: type, abortTrigger: abortTrigger,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -531,8 +531,11 @@ class MemoriesApi { // 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), 'MemorySearchResponseDto',) as MemorySearchResponseDto; - + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + } return null; } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index f3a71fe17e..90bff45431 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -451,8 +451,6 @@ class ApiClient { return MemoryResponseDto.fromJson(value); case 'MemorySearchOrder': return MemorySearchOrderTypeTransformer().decode(value); - case 'MemorySearchResponseDto': - return MemorySearchResponseDto.fromJson(value); case 'MemoryStatisticsResponseDto': return MemoryStatisticsResponseDto.fromJson(value); case 'MemoryType': diff --git a/mobile/openapi/lib/model/memory_search_response_dto.dart b/mobile/openapi/lib/model/memory_search_response_dto.dart deleted file mode 100644 index 82e04564cf..0000000000 --- a/mobile/openapi/lib/model/memory_search_response_dto.dart +++ /dev/null @@ -1,120 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// 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 MemorySearchResponseDto { - /// Returns a new [MemorySearchResponseDto] instance. - MemorySearchResponseDto({ - required this.hasNextPage, - this.items = const [], - required this.total, - }); - - /// Whether there are more pages - bool hasNextPage; - - List items; - - /// Total number of matching memories - /// - /// Minimum value: 0 - /// Maximum value: 9007199254740991 - int total; - - @override - bool operator ==(Object other) => identical(this, other) || other is MemorySearchResponseDto && - other.hasNextPage == hasNextPage && - _deepEquality.equals(other.items, items) && - other.total == total; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (hasNextPage.hashCode) + - (items.hashCode) + - (total.hashCode); - - @override - String toString() => 'MemorySearchResponseDto[hasNextPage=$hasNextPage, items=$items, total=$total]'; - - Map toJson() { - final json = {}; - json[r'hasNextPage'] = this.hasNextPage; - json[r'items'] = this.items; - json[r'total'] = this.total; - return json; - } - - /// Returns a new [MemorySearchResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static MemorySearchResponseDto? fromJson(dynamic value) { - upgradeDto(value, "MemorySearchResponseDto"); - if (value is Map) { - final json = value.cast(); - - return MemorySearchResponseDto( - hasNextPage: mapValueOfType(json, r'hasNextPage')!, - items: MemoryResponseDto.listFromJson(json[r'items']), - total: mapValueOfType(json, r'total')!, - ); - } - 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 = MemorySearchResponseDto.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 = MemorySearchResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of MemorySearchResponseDto-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] = MemorySearchResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'hasNextPage', - 'items', - 'total', - }; -} - diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index c38ba67f59..cb49af2164 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7207,7 +7207,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MemorySearchResponseDto" + "items": { + "$ref": "#/components/schemas/MemoryResponseDto" + }, + "type": "array" } } }, @@ -19974,32 +19977,6 @@ ], "type": "string" }, - "MemorySearchResponseDto": { - "properties": { - "hasNextPage": { - "description": "Whether there are more pages", - "type": "boolean" - }, - "items": { - "items": { - "$ref": "#/components/schemas/MemoryResponseDto" - }, - "type": "array" - }, - "total": { - "description": "Total number of matching memories", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "hasNextPage", - "items", - "total" - ], - "type": "object" - }, "MemoryStatisticsResponseDto": { "properties": { "total": { diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 813c514241..a9c40ea8b8 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -1350,13 +1350,6 @@ export type MemoryResponseDto = { /** Last update date */ updatedAt: string; }; -export type MemorySearchResponseDto = { - /** Whether there are more pages */ - hasNextPage: boolean; - items: MemoryResponseDto[]; - /** Total number of matching memories */ - total: number; -}; export type MemoryCreateDto = { /** Asset IDs to associate with memory */ assetIds?: string[]; @@ -4988,7 +4981,7 @@ export function searchMemories({ $for, isSaved, isTrashed, order, page, size, $t }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: MemorySearchResponseDto; + data: MemoryResponseDto[]; }>(`/memories${QS.query(QS.explode({ "for": $for, isSaved, diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index 4a41ce0f0f..66a3012888 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -7,7 +7,6 @@ import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, - MemorySearchResponseDto, MemoryStatisticsResponseDto, MemoryUpdateDto, } from 'src/dtos/memory.dto'; @@ -29,7 +28,7 @@ export class MemoryController { 'Retrieve a list of memories. Memories are sorted descending by creation date by default, although they can also be sorted in ascending order, or randomly.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { + searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { return this.service.search(auth, dto); } diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 335fd9d64a..2b82a9ba64 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -76,20 +76,11 @@ const MemoryResponseSchema = z }) .meta({ id: 'MemoryResponseDto' }); -const MemorySearchResponseSchema = z - .object({ - total: z.int().min(0).describe('Total number of matching memories'), - items: z.array(MemoryResponseSchema), - hasNextPage: z.boolean().describe('Whether there are more pages'), - }) - .meta({ id: 'MemorySearchResponseDto' }); - export class MemorySearchDto extends createZodDto(MemorySearchSchema) {} export class MemoryUpdateDto extends createZodDto(MemoryUpdateSchema) {} export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {} export class MemoryStatisticsResponseDto extends createZodDto(MemoryStatisticsResponseSchema) {} export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {} -export class MemorySearchResponseDto extends createZodDto(MemorySearchResponseSchema) {} export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => { return { diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index b95cb495ad..2de0d5755e 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -58,8 +58,8 @@ export class MemoryRepository implements IBulkAsset { { params: [DummyValue.UUID, {}] }, { name: 'date filter', params: [DummyValue.UUID, { for: DummyValue.DATE }] }, ) - async search(ownerId: string, dto: MemorySearchDto) { - const items = await this.searchBuilder(ownerId, dto) + search(ownerId: string, dto: MemorySearchDto) { + return this.searchBuilder(ownerId, dto) .select((eb) => jsonArrayFrom( eb @@ -90,11 +90,9 @@ export class MemoryRepository implements IBulkAsset { ? qb.orderBy(sql`RANDOM()`) : qb.orderBy('memoryAt', (dto.order?.toLowerCase() || 'desc') as OrderByDirection), ) - .$if(dto.size !== undefined, (qb) => qb.limit(dto.size! + 1)) + .$if(dto.size !== undefined, (qb) => qb.limit(dto.size!)) .$if(dto.page !== undefined && dto.size !== undefined, (qb) => qb.offset((dto.page! - 1) * dto.size!)) .execute(); - - return paginationHelper(items, dto.size ?? items.length); } @GenerateSql({ params: [DummyValue.UUID] }) diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index cda4184e1f..cc35c672f9 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -34,28 +34,24 @@ describe(MemoryService.name, () => { const asset = AssetFactory.create(); const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build(); const memory2 = MemoryFactory.create({ ownerId: userId }); - mocks.memory.search.mockResolvedValue({ - items: [getForMemory(memory1), getForMemory(memory2)], - hasNextPage: false, - }); + mocks.memory.search.mockResolvedValue([getForMemory(memory1), getForMemory(memory2)]); mocks.memory.statistics.mockResolvedValue({ total: 2 }); - await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toMatchObject({ - items: expect.arrayContaining([ + await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual( + expect.arrayContaining([ expect.objectContaining({ id: memory1.id, assets: expect.arrayContaining([expect.objectContaining({ id: asset.id })]), }), ]), - hasNextPage: false, - total: 2, - }); + ); + mocks.memory.search.mockResolvedValue([]); + await expect(sut.search(factory.auth(), {})).resolves.toEqual([]); }); it('should map empty result', async () => { - mocks.memory.search.mockResolvedValue({ items: [], hasNextPage: false }); - mocks.memory.statistics.mockResolvedValue({ total: 0 }); - await expect(sut.search(factory.auth(), {})).resolves.toMatchObject({ items: [], hasNextPage: false, total: 0 }); + mocks.memory.search.mockResolvedValue([]); + await expect(sut.search(factory.auth(), {})).resolves.toEqual([]); }); }); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 0fef2c9b5c..ac8f88ad87 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -71,14 +71,10 @@ export class MemoryService extends BaseService { } async search(auth: AuthDto, dto: MemorySearchDto) { - const { items, hasNextPage } = await this.memoryRepository.search(auth.user.id, dto); - const { total } = await this.memoryRepository.statistics(auth.user.id, dto); - - return { - total, - items: items.map((memory: Memory) => mapMemory(memory, auth)), - hasNextPage, - }; + const memories = await this.memoryRepository.search(auth.user.id, dto); + return memories + .filter((memory: Memory) => memory.assets && memory.assets.length > 0) + .map((memory: Memory) => mapMemory(memory, auth)); } statistics(auth: AuthDto, dto: MemorySearchDto) { diff --git a/server/test/medium/specs/services/memory.service.spec.ts b/server/test/medium/specs/services/memory.service.spec.ts index a134e0735d..b3a3da6010 100644 --- a/server/test/medium/specs/services/memory.service.spec.ts +++ b/server/test/medium/specs/services/memory.service.spec.ts @@ -133,8 +133,8 @@ describe(MemoryService.name, () => { await sut.onMemoriesCreate(); const memories = await memoryRepo.search(user.id, {}); - expect(memories.items.length).toBe(1); - expect(memories.items[0]).toEqual( + expect(memories.length).toBe(1); + expect(memories[0]).toEqual( expect.objectContaining({ id: expect.any(String), createdAt: expect.any(Date), @@ -173,8 +173,8 @@ describe(MemoryService.name, () => { await sut.onMemoriesCreate(); const memories = await memoryRepo.search(user.id, {}); - expect(memories.items.length).toBe(1); - expect(memories.items[0]).toEqual( + expect(memories.length).toBe(1); + expect(memories[0]).toEqual( expect.objectContaining({ id: expect.any(String), createdAt: expect.any(Date), @@ -228,12 +228,12 @@ describe(MemoryService.name, () => { await sut.onMemoriesCreate(); const memories = await memoryRepo.search(user.id, {}); - expect(memories.items.length).toBe(1); + expect(memories.length).toBe(1); await sut.onMemoriesCreate(); const memoriesAfter = await memoryRepo.search(user.id, {}); - expect(memoriesAfter.items.length).toBe(1); + expect(memoriesAfter.length).toBe(1); }); }); diff --git a/web/src/lib/managers/memory-manager.svelte.ts b/web/src/lib/managers/memory-manager.svelte.ts index a3cdc0f9cc..1ceb577e20 100644 --- a/web/src/lib/managers/memory-manager.svelte.ts +++ b/web/src/lib/managers/memory-manager.svelte.ts @@ -6,6 +6,7 @@ import { updateMemory, MemorySearchOrder, MemoryType, + memoriesStatistics, } from '@immich/sdk'; import { DateTime } from 'luxon'; import { authManager } from '$lib/managers/auth-manager.svelte'; @@ -191,10 +192,15 @@ class MemoryManager { private async load(page: number) { if (this.#filters !== undefined) { - const { items, hasNextPage, total } = await searchMemories({ size: PAGE_SIZE, ...this.#filters, page }); + const items = await searchMemories({ size: PAGE_SIZE, ...this.#filters, page }); this.memories.push(...items); - this.#hasNextPage = hasNextPage; - this.#total = total; + + if (this.#total === undefined) { + const { total } = await memoriesStatistics(this.#filters); + this.#total = total; + } + + this.#hasNextPage = this.memories.length < this.#total; } }