From e88bd74fd2ab7ce8c3a07a2782e85f8d7598415f Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Tue, 10 Jun 2025 23:47:46 +1000 Subject: [PATCH] feat(server): add memories statistics resource (#19035) --- mobile/openapi/README.md | Bin 37176 -> 37361 bytes mobile/openapi/lib/api.dart | Bin 13278 -> 13328 bytes mobile/openapi/lib/api/memories_api.dart | Bin 12514 -> 14818 bytes mobile/openapi/lib/api_client.dart | Bin 33489 -> 33595 bytes .../model/memory_statistics_response_dto.dart | Bin 0 -> 3023 bytes open-api/immich-openapi-specs.json | 77 ++++++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 21 +++++ server/src/controllers/memory.controller.ts | 14 +++- server/src/dtos/memory.dto.ts | 5 ++ server/src/queries/memory.repository.sql | 34 +++++++- server/src/repositories/memory.repository.ts | 38 ++++++--- server/src/services/memory.service.ts | 4 + 12 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 mobile/openapi/lib/model/memory_statistics_response_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index ee646354d1d58e01b59531efbb629017ca2cda70..c5cff5176a519e48bee688d4f621312c82726fe9 100644 GIT binary patch delta 100 zcmdn7i0R{ErVR$Fg1M==`9+zj#laV}eOZTfv iY&KAx=+24Iaj-&|)fZ&W+NOx~@ delta 16 XcmaD<{3vllr{v@ue$mbIlw8CCMZpH# diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index bc7f48255c867bcb112b8bb30414eeaba229f5d5..28b67f52c53ce085840e4bf8cd63076b4c5c3efe 100644 GIT binary patch delta 30 lcmccE%Cx(UX~S`w$xbqwlPB1)0cp|6I@$`G584R00RXyZ3giF) delta 14 Vcmdnp#&ofjX~S`w&4#vIZU8YF1;79R 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 0000000000000000000000000000000000000000..a9a10ad327d8ade4e95078a5f0930a29d9163afc GIT binary patch literal 3023 zcmbVOZExE)5dQ98aVdsG0aSV0ry_~H7K=0VB{7hA0|vto7>Tmk$)ZM5HH_5%eRoGm zmQh#98X$=!-|O={cjRz57!KjxpR?)fKPGpR`**X+HQc=aGzsB)3U|{P+)l6G-~4@o zW@Py`6~>KzjedDEphvYVw2^V9O`NG*UO-h;R;IDcs+ny_dGc#PZr{cp3l<5sQ22 zDpObxP{<9;dt4TvxadSi=-Xo$P;7t(7jP?OBd49nL;PZQj`ab?a=`ES>8<`EjX^N* zn@6p4YR3qt4U9&?^zpMyut~R)<{U<2@Sr6k4=8lleDn3I{{*VP_0vd#3o}F72k$4K3NNa`J68XsIN?K*vEZ`QS#@n~vZT7(#RV*>N{}6D0 zXFcG~IpXLi{U?hVAIZj>!B`A%Q4T@Ka_%ISXGzUP%j?(d#TeqfAH>8)aFN9ZpIkGn^G3GgBofBpZ{&Vw^ghgct{OrBmX_zvz?mZ(I(8S%d4# zY>=(Wr4cC&Rlv1|@T!NuSEvD+4(!L9vsy*jL<2ZriX%peujA@vs9=nXC?Hr;>UFvFB!1w92sBw+3l+o{nbAorx7j99?k=(I7Zxk#j` zmqy3aV^8BoaSoWMy_}{Fg9gA0Xg+Zi9@9qaopsUE#F#X4l&q#|4;ESxAI$>li`c9g z!lB^>r@5CFwi|iczBdQBfa*j20^0L8#v>3zy92L`COFfI!8q>B>;M%f)v4=Jv95LrWHbyP6! zI}7DmwPA~gD(ImqB05_@JE9BQncq);*edbJ>cFfW0S#fclC4(S>l@0_;w?a+4l0z< tqXax%EvBXE5%A$*dPf(+S{{7lws*geCgZG literal 0 HcmV?d00001 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);