From d92df63f844fdff7501acf9161c293e45d92df4c Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Tue, 11 Nov 2025 01:38:50 +1100 Subject: [PATCH] feat: random memories sort order (#20025) --- mobile/openapi/README.md | Bin 41446 -> 41496 bytes mobile/openapi/lib/api.dart | Bin 14752 -> 14791 bytes mobile/openapi/lib/api/memories_api.dart | Bin 15709 -> 16683 bytes mobile/openapi/lib/api_client.dart | Bin 36620 -> 36721 bytes mobile/openapi/lib/api_helper.dart | Bin 7088 -> 7200 bytes .../lib/model/memory_search_order.dart | Bin 0 -> 2821 bytes open-api/immich-openapi-specs.json | 44 ++++++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 17 ++++++- server/src/dtos/memory.dto.ts | 11 ++++- server/src/enum.ts | 8 ++++ server/src/repositories/memory.repository.ts | 11 +++-- 11 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 mobile/openapi/lib/model/memory_search_order.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 85ec031736152dfd5ba774abfa245cee626fcde9..33ccadba7ec4c5b423d9b41e7dfb3fbf3eba71d6 100644 GIT binary patch delta 49 xcmaEMm}$llrVR%wlu{Fmk~92^Qc{azHB$1E_4R#IbMuQTgAwAJH&mEu0sst?6fpn* delta 26 icmbPngz4E~rVR%wCW};xOm?VepFF>UZL@GCt0n-WKnk1y diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index ab88670bcd7bf8a8ba6c61904ef3d3b8055459b5..a4b58763c147c13eb3a6d7b8bfa27ff921f53a40 100644 GIT binary patch delta 28 jcmZ2be7tx=zXW$`Vo`ENe11_%YSCmxana2k5)z64t{w`S delta 12 TcmX?Jyr6hPzr^Nk5`u~VDR%`} diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index f9280101e65e45fa07f3acdc0ce8789da1d12eeb..9916029e0bebbabd3483ad32971e986c876c6783 100644 GIT binary patch delta 725 zcmcaxwYrIMgRx9daB5;va)y6VN@`K8LOzJH;!;r1*Vl)T3R()$nRz9#3dNaKsZbdO z1qHv-+@#bZh5R&y+|=CsqRiA{g_3-QqSTVoqP)rP6|^UN$Rx@l8Ep?ST1NqBmOa?4 z%?o8Bm{`FUPQE8=t_)_W>p;|LYFY!WS5T-0lbLA>8X)UI>bOA+pfV7(`MI1N6Ky;& z1;67bFO*T9JWnxO0uo>_&w;}T;xu-p2yWUqO?z^QZY)9PrEfOSd&Wp{_|`B^ZZMof dD~}i&5lAG^a2B6@#3%q?SifQ1{Ldtw0|53+@Us8_ delta 103 zcmZ48#CW%AgR#uyb^7v?cgcirHj@ot+H7l_#W*=kT4i#Amh|Kc(#b$s(aDFDGAFy4 z8BK0d4%z(3>=onYTdFynK$)V+X0q{ch7agUzgOT_RE&7`o9l8Agld}-; delta 14 Vcmex3kEv%K(*_R5&AE;&egH2f1!@2Q diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index b34e9210c8649083bed8a9136ca8ab249cb9da0e..1d197a8f91104d6a38dd5f037a288cbb63a1b6d4 100644 GIT binary patch delta 62 zcmdmBzQAI`a~{p$)Wo9X4F95()FMrVYAyvJC`v6UEy`2yP0h_Os)Wl=P7pPje1Ma8 J^FyBVd;rnV7XknP delta 12 TcmZ2rvB7-9bDquNyeIhpC4L1_ diff --git a/mobile/openapi/lib/model/memory_search_order.dart b/mobile/openapi/lib/model/memory_search_order.dart new file mode 100644 index 0000000000000000000000000000000000000000..bdf5b59894439a4b43842096eb39459a077b3f6e GIT binary patch literal 2821 zcma)8ZBOGy5dNNDF-X-Bq||Bq>BKe1ArV@tJE%(dP(_io*lV&qd)M7v6GbQf`#rO3 z8;BEHB$9P@UZ0tH#?$F&I;Hcw#m$eu&9CQwoiFAWbb0l8KB0>ny1rS^mz#^L%fF8x z#*&|MVg2lvwMy{+VKf?LrEreE$jaF5trRv}-`HGQ`Zgk(0)ngPUkNWskd_;g zx~XWR{F?k)QfLA1aA!x_DM38=lw+W3Xb zXtp;CdyKndWI*&vBbRTe@Z}yID|BwEgO1`g>$PZoRxXrLlqJ>1Idxykn4Tl0BWO!5 z(P}v;YADXDhy&OT-hUkke_X;N5i*^G1IHgvF(y8*LRhWEfrT?Ftf>Od`4&A7WMk>M zA_M8gww4Q+c9^zG+VM$}%gp3*{Gwf2PmIByfT_gqm%A?iL7gY`?cSJ@9<13225d%! zD4m=Pu5NR|On1&!Gzp*uq{-n}D!PL#VgeqPmn+ZlyfV`&_l7OuHK zEc$%kIgU-guiJVEp&vSqV~9~irK1_?rYv9DB#Mb7BF}sHq8`yx$fEm>PgeTI>Q2iW z&THI(b=t!g{6CGJm>Z|m7Yj23I3kgNbUbB36MB%Vw<4TX;E+u6RPX6Z@}`~INid9w zpk*xnin`9pg%@-$vac+?$4GLB$#(iG)VmOw2jBgVCgfI4`}oh!W{1da#|^kH&g=m$ zJS5)IQv`237!qO_0;BOEGHFi@7hzr;XnNV97FuMrWZ@M9Cx$6At)iUqlde!9P>b1I zqo-EfV%^jd9jOKkh2eCGTjjFDRIe%h|IYa~TWLfJ7V?e726aFR3LHPEy=|r)?33 z!6aB3kt~^XT9vT>-q7Lufl|Bv-cXAY&x*M$1UUQ@Aa#G7#;h;_vD#O^+&9~iT^%R3)$uqv}N#|PSsgK&!qm(qhXhdj2!3uw-k TJGttI7VVDG*)wB(V6y%Wu%4>2 literal 0 HcmV?d00001 diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6076b43bf..ed3d7a0ba 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4268,6 +4268,24 @@ "type": "boolean" } }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/MemorySearchOrder" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "description": "Number of memories to return", + "schema": { + "minimum": 1, + "type": "integer" + } + }, { "name": "type", "required": false, @@ -4381,6 +4399,24 @@ "type": "boolean" } }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/MemorySearchOrder" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "description": "Number of memories to return", + "schema": { + "minimum": 1, + "type": "integer" + } + }, { "name": "type", "required": false, @@ -12780,6 +12816,14 @@ ], "type": "object" }, + "MemorySearchOrder": { + "enum": [ + "asc", + "desc", + "random" + ], + "type": "string" + }, "MemoryStatisticsResponseDto": { "properties": { "total": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 1ed65e4f2..a0f43c620 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2956,10 +2956,12 @@ export function reverseGeocode({ lat, lon }: { /** * This endpoint requires the `memory.read` permission. */ -export function searchMemories({ $for, isSaved, isTrashed, $type }: { +export function searchMemories({ $for, isSaved, isTrashed, order, size, $type }: { $for?: string; isSaved?: boolean; isTrashed?: boolean; + order?: MemorySearchOrder; + size?: number; $type?: MemoryType; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ @@ -2969,6 +2971,8 @@ export function searchMemories({ $for, isSaved, isTrashed, $type }: { "for": $for, isSaved, isTrashed, + order, + size, "type": $type }))}`, { ...opts @@ -2992,10 +2996,12 @@ export function createMemory({ memoryCreateDto }: { /** * This endpoint requires the `memory.statistics` permission. */ -export function memoriesStatistics({ $for, isSaved, isTrashed, $type }: { +export function memoriesStatistics({ $for, isSaved, isTrashed, order, size, $type }: { $for?: string; isSaved?: boolean; isTrashed?: boolean; + order?: MemorySearchOrder; + size?: number; $type?: MemoryType; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ @@ -3005,6 +3011,8 @@ export function memoriesStatistics({ $for, isSaved, isTrashed, $type }: { "for": $for, isSaved, isTrashed, + order, + size, "type": $type }))}`, { ...opts @@ -4991,6 +4999,11 @@ export enum JobCommand { Empty = "empty", ClearFailed = "clear-failed" } +export enum MemorySearchOrder { + Asc = "asc", + Desc = "desc", + Random = "random" +} export enum MemoryType { OnThisDay = "on_this_day" } diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index a79511c73..8a86e6669 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -4,7 +4,7 @@ import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { Memory } from 'src/database'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MemoryType } from 'src/enum'; +import { AssetOrderWithRandom, MemoryType } from 'src/enum'; import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; class MemoryBaseDto { @@ -27,6 +27,15 @@ export class MemorySearchDto { @ValidateBoolean({ optional: true }) isSaved?: boolean; + + @IsInt() + @IsPositive() + @Type(() => Number) + @ApiProperty({ type: 'integer', description: 'Number of memories to return' }) + size?: number; + + @ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', optional: true }) + order?: AssetOrderWithRandom; } class OnThisDayDto { diff --git a/server/src/enum.ts b/server/src/enum.ts index 0755f75f7..a98fee011 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -71,6 +71,14 @@ export enum MemoryType { OnThisDay = 'on_this_day', } +export enum AssetOrderWithRandom { + // Include existing values + Asc = AssetOrder.Asc, + Desc = AssetOrder.Desc, + /** Randomly Ordered */ + Random = 'random', +} + export enum Permission { All = 'all', diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index c4a914453..e62c08383 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, sql, Updateable } from 'kysely'; +import { Insertable, Kysely, OrderByDirection, sql, Updateable } from 'kysely'; import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { DateTime } from 'luxon'; import { InjectKysely } from 'nestjs-kysely'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { MemorySearchDto } from 'src/dtos/memory.dto'; -import { AssetVisibility } from 'src/enum'; +import { AssetOrderWithRandom, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; import { MemoryTable } from 'src/schema/tables/memory.table'; import { IBulkAsset } from 'src/types'; @@ -72,7 +72,12 @@ export class MemoryRepository implements IBulkAsset { ).as('assets'), ) .selectAll('memory') - .orderBy('memoryAt', 'desc') + .$call((qb) => + dto.order === AssetOrderWithRandom.Random + ? qb.orderBy(sql`RANDOM()`) + : qb.orderBy('memoryAt', (dto.order?.toLowerCase() || 'desc') as OrderByDirection), + ) + .$if(dto.size !== undefined, (qb) => qb.limit(dto.size!)) .execute(); }