diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index ee646354d..c5cff5176 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 9bf402632..573081503 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 88897d303..9b62cce9c 100644 Binary files a/mobile/openapi/lib/api/memories_api.dart and b/mobile/openapi/lib/api/memories_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index bc7f48255..28b67f52c 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/memory_statistics_response_dto.dart b/mobile/openapi/lib/model/memory_statistics_response_dto.dart new file mode 100644 index 000000000..a9a10ad32 Binary files /dev/null and b/mobile/openapi/lib/model/memory_statistics_response_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 775fe117a..4f0fa44fa 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3599,6 +3599,72 @@ ] } }, + "/memories/statistics": { + "get": { + "operationId": "memoriesStatistics", + "parameters": [ + { + "name": "for", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "isSaved", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isTrashed", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/MemoryType" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryStatisticsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memories" + ] + } + }, "/memories/{id}": { "delete": { "operationId": "deleteMemory", @@ -10827,6 +10893,17 @@ ], "type": "object" }, + "MemoryStatisticsResponseDto": { + "properties": { + "total": { + "type": "integer" + } + }, + "required": [ + "total" + ], + "type": "object" + }, "MemoryType": { "enum": [ "on_this_day" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index fee00acff..417055e48 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -742,6 +742,9 @@ export type MemoryCreateDto = { seenAt?: string; "type": MemoryType; }; +export type MemoryStatisticsResponseDto = { + total: number; +}; export type MemoryUpdateDto = { isSaved?: boolean; memoryAt?: string; @@ -2509,6 +2512,24 @@ export function createMemory({ memoryCreateDto }: { body: memoryCreateDto }))); } +export function memoriesStatistics({ $for, isSaved, isTrashed, $type }: { + $for?: string; + isSaved?: boolean; + isTrashed?: boolean; + $type?: MemoryType; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryStatisticsResponseDto; + }>(`/memories/statistics${QS.query(QS.explode({ + "for": $for, + isSaved, + isTrashed, + "type": $type + }))}`, { + ...opts + })); +} export function deleteMemory({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index 1f848ad70..d33c5ec22 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -2,7 +2,13 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, import { ApiTags } from '@nestjs/swagger'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; +import { + MemoryCreateDto, + MemoryResponseDto, + MemorySearchDto, + MemoryStatisticsResponseDto, + MemoryUpdateDto, +} from 'src/dtos/memory.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MemoryService } from 'src/services/memory.service'; @@ -25,6 +31,12 @@ export class MemoryController { return this.service.create(auth, dto); } + @Get('statistics') + @Authenticated({ permission: Permission.MEMORY_READ }) + memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise { + return this.service.statistics(auth, dto); + } + @Get(':id') @Authenticated({ permission: Permission.MEMORY_READ }) getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 98231a903..675039363 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -71,6 +71,11 @@ export class MemoryCreateDto extends MemoryBaseDto { assetIds?: string[]; } +export class MemoryStatisticsResponseDto { + @ApiProperty({ type: 'integer' }) + total!: number; +} + export class MemoryResponseDto { id!: string; createdAt!: Date; diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index a3243025b..3cdda8a45 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -1,8 +1,33 @@ -- NOTE: This file is auto generated by ./sql-generator +-- MemoryRepository.statistics +select + count(*) as "total" +from + "memories" +where + "deletedAt" is null + and "ownerId" = $1 + +-- MemoryRepository.statistics (date filter) +select + count(*) as "total" +from + "memories" +where + ( + "showAt" is null + or "showAt" <= $1 + ) + and ( + "hideAt" is null + or "hideAt" >= $2 + ) + and "deletedAt" is null + and "ownerId" = $3 + -- MemoryRepository.search select - "memories".*, ( select coalesce(json_agg(agg), '[]') @@ -20,7 +45,8 @@ select order by "assets"."fileCreatedAt" asc ) as agg - ) as "assets" + ) as "assets", + "memories".* from "memories" where @@ -31,7 +57,6 @@ order by -- MemoryRepository.search (date filter) select - "memories".*, ( select coalesce(json_agg(agg), '[]') @@ -49,7 +74,8 @@ select order by "assets"."fileCreatedAt" asc ) as agg - ) as "assets" + ) as "assets", + "memories".* from "memories" where diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 96eb78e6d..0f29b3746 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -28,14 +28,36 @@ export class MemoryRepository implements IBulkAsset { .execute(); } + searchBuilder(ownerId: string, dto: MemorySearchDto) { + return this.db + .selectFrom('memories') + .$if(dto.isSaved !== undefined, (qb) => qb.where('isSaved', '=', dto.isSaved!)) + .$if(dto.type !== undefined, (qb) => qb.where('type', '=', dto.type!)) + .$if(dto.for !== undefined, (qb) => + qb + .where((where) => where.or([where('showAt', 'is', null), where('showAt', '<=', dto.for!)])) + .where((where) => where.or([where('hideAt', 'is', null), where('hideAt', '>=', dto.for!)])), + ) + .where('deletedAt', dto.isTrashed ? 'is not' : 'is', null) + .where('ownerId', '=', ownerId); + } + + @GenerateSql( + { params: [DummyValue.UUID, {}] }, + { name: 'date filter', params: [DummyValue.UUID, { for: DummyValue.DATE }] }, + ) + statistics(ownerId: string, dto: MemorySearchDto) { + return this.searchBuilder(ownerId, dto) + .select((qb) => qb.fn.countAll().as('total')) + .executeTakeFirstOrThrow(); + } + @GenerateSql( { params: [DummyValue.UUID, {}] }, { name: 'date filter', params: [DummyValue.UUID, { for: DummyValue.DATE }] }, ) search(ownerId: string, dto: MemorySearchDto) { - return this.db - .selectFrom('memories') - .selectAll('memories') + return this.searchBuilder(ownerId, dto) .select((eb) => jsonArrayFrom( eb @@ -48,15 +70,7 @@ export class MemoryRepository implements IBulkAsset { .where('assets.deletedAt', 'is', null), ).as('assets'), ) - .$if(dto.isSaved !== undefined, (qb) => qb.where('isSaved', '=', dto.isSaved!)) - .$if(dto.type !== undefined, (qb) => qb.where('type', '=', dto.type!)) - .$if(dto.for !== undefined, (qb) => - qb - .where((where) => where.or([where('showAt', 'is', null), where('showAt', '<=', dto.for!)])) - .where((where) => where.or([where('hideAt', 'is', null), where('hideAt', '>=', dto.for!)])), - ) - .where('deletedAt', dto.isTrashed ? 'is not' : 'is', null) - .where('ownerId', '=', ownerId) + .selectAll('memories') .orderBy('memoryAt', 'desc') .execute(); } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 1ccd31179..b0ea697ed 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -82,6 +82,10 @@ export class MemoryService extends BaseService { return memories.map((memory) => mapMemory(memory, auth)); } + statistics(auth: AuthDto, dto: MemorySearchDto) { + return this.memoryRepository.statistics(auth.user.id, dto); + } + async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id);