diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 698b9774da..03a1ba9f17 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -402,6 +402,7 @@ Class | Method | HTTP request | Description - [MemoriesUpdate](doc//MemoriesUpdate.md) - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryResponseDto](doc//MemoryResponseDto.md) + - [MemorySearchOrder](doc//MemorySearchOrder.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 a3117ede86..c1ed06984e 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -173,6 +173,7 @@ part 'model/memories_response.dart'; 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_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 f9280101e6..9916029e0b 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -238,8 +238,13 @@ class MemoriesApi { /// /// * [bool] isTrashed: /// + /// * [MemorySearchOrder] order: + /// + /// * [int] size: + /// Number of memories to return + /// /// * [MemoryType] type: - Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async { + Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -259,6 +264,12 @@ class MemoriesApi { if (isTrashed != null) { queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } if (type != null) { queryParams.addAll(_queryParams('', 'type', type)); } @@ -287,9 +298,14 @@ class MemoriesApi { /// /// * [bool] isTrashed: /// + /// * [MemorySearchOrder] order: + /// + /// * [int] size: + /// Number of memories to return + /// /// * [MemoryType] type: - Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async { - final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, type: type, ); + Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { + final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -375,8 +391,13 @@ class MemoriesApi { /// /// * [bool] isTrashed: /// + /// * [MemorySearchOrder] order: + /// + /// * [int] size: + /// Number of memories to return + /// /// * [MemoryType] type: - Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async { + Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -396,6 +417,12 @@ class MemoriesApi { if (isTrashed != null) { queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } if (type != null) { queryParams.addAll(_queryParams('', 'type', type)); } @@ -424,9 +451,14 @@ class MemoriesApi { /// /// * [bool] isTrashed: /// + /// * [MemorySearchOrder] order: + /// + /// * [int] size: + /// Number of memories to return + /// /// * [MemoryType] type: - Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async { - final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, type: type, ); + Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { + final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 33056cf14e..bd57177877 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -400,6 +400,8 @@ class ApiClient { return MemoryCreateDto.fromJson(value); case 'MemoryResponseDto': return MemoryResponseDto.fromJson(value); + case 'MemorySearchOrder': + return MemorySearchOrderTypeTransformer().decode(value); case 'MemoryStatisticsResponseDto': return MemoryStatisticsResponseDto.fromJson(value); case 'MemoryType': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index b34e9210c8..1d197a8f91 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -106,6 +106,9 @@ String parameterToString(dynamic value) { if (value is ManualJobName) { return ManualJobNameTypeTransformer().encode(value).toString(); } + if (value is MemorySearchOrder) { + return MemorySearchOrderTypeTransformer().encode(value).toString(); + } if (value is MemoryType) { return MemoryTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 7d50259e24..b1ea648e11 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -16,14 +16,10 @@ class MemoryResponseDto { this.assets = const [], required this.createdAt, required this.data, - this.deletedAt, - this.hideAt, required this.id, required this.isSaved, required this.memoryAt, required this.ownerId, - this.seenAt, - this.showAt, required this.type, required this.updatedAt, }); @@ -34,22 +30,6 @@ class MemoryResponseDto { OnThisDayDto data; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - DateTime? deletedAt; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - DateTime? hideAt; - String id; bool isSaved; @@ -58,22 +38,6 @@ class MemoryResponseDto { String ownerId; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - DateTime? seenAt; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - DateTime? showAt; - MemoryType type; DateTime updatedAt; @@ -83,14 +47,10 @@ class MemoryResponseDto { _deepEquality.equals(other.assets, assets) && other.createdAt == createdAt && other.data == data && - other.deletedAt == deletedAt && - other.hideAt == hideAt && other.id == id && other.isSaved == isSaved && other.memoryAt == memoryAt && other.ownerId == ownerId && - other.seenAt == seenAt && - other.showAt == showAt && other.type == type && other.updatedAt == updatedAt; @@ -100,49 +60,25 @@ class MemoryResponseDto { (assets.hashCode) + (createdAt.hashCode) + (data.hashCode) + - (deletedAt == null ? 0 : deletedAt!.hashCode) + - (hideAt == null ? 0 : hideAt!.hashCode) + (id.hashCode) + (isSaved.hashCode) + (memoryAt.hashCode) + (ownerId.hashCode) + - (seenAt == null ? 0 : seenAt!.hashCode) + - (showAt == null ? 0 : showAt!.hashCode) + (type.hashCode) + (updatedAt.hashCode); @override - String toString() => 'MemoryResponseDto[assets=$assets, createdAt=$createdAt, data=$data, deletedAt=$deletedAt, hideAt=$hideAt, id=$id, isSaved=$isSaved, memoryAt=$memoryAt, ownerId=$ownerId, seenAt=$seenAt, showAt=$showAt, type=$type, updatedAt=$updatedAt]'; + String toString() => 'MemoryResponseDto[assets=$assets, createdAt=$createdAt, data=$data, id=$id, isSaved=$isSaved, memoryAt=$memoryAt, ownerId=$ownerId, type=$type, updatedAt=$updatedAt]'; Map toJson() { final json = {}; json[r'assets'] = this.assets; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; - if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); - } else { - // json[r'deletedAt'] = null; - } - if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); - } else { - // json[r'hideAt'] = null; - } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; - if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); - } else { - // json[r'seenAt'] = null; - } - if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); - } else { - // json[r'showAt'] = null; - } json[r'type'] = this.type; json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); return json; @@ -160,14 +96,10 @@ class MemoryResponseDto { assets: AssetResponseDto.listFromJson(json[r'assets']), createdAt: mapDateTime(json, r'createdAt', r'')!, data: OnThisDayDto.fromJson(json[r'data'])!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, memoryAt: mapDateTime(json, r'memoryAt', r'')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), type: MemoryType.fromJson(json[r'type'])!, updatedAt: mapDateTime(json, r'updatedAt', r'')!, ); 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 0000000000..bdf5b59894 --- /dev/null +++ b/mobile/openapi/lib/model/memory_search_order.dart @@ -0,0 +1,88 @@ +// +// 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 MemorySearchOrder { + /// Instantiate a new enum with the provided [value]. + const MemorySearchOrder._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const asc = MemorySearchOrder._(r'asc'); + static const desc = MemorySearchOrder._(r'desc'); + static const random = MemorySearchOrder._(r'random'); + + /// List of all possible values in this [enum][MemorySearchOrder]. + static const values = [ + asc, + desc, + random, + ]; + + static MemorySearchOrder? fromJson(dynamic value) => MemorySearchOrderTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemorySearchOrder.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemorySearchOrder] to String, +/// and [decode] dynamic data back to [MemorySearchOrder]. +class MemorySearchOrderTypeTransformer { + factory MemorySearchOrderTypeTransformer() => _instance ??= const MemorySearchOrderTypeTransformer._(); + + const MemorySearchOrderTypeTransformer._(); + + String encode(MemorySearchOrder data) => data.value; + + /// Decodes a [dynamic value][data] to a MemorySearchOrder. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemorySearchOrder? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'asc': return MemorySearchOrder.asc; + case r'desc': return MemorySearchOrder.desc; + case r'random': return MemorySearchOrder.random; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemorySearchOrderTypeTransformer] instance. + static MemorySearchOrderTypeTransformer? _instance; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index fdfc40eb6c..ac1e4bd8de 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4136,6 +4136,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, @@ -4249,6 +4267,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, @@ -12441,14 +12477,6 @@ "data": { "$ref": "#/components/schemas/OnThisDayDto" }, - "deletedAt": { - "format": "date-time", - "type": "string" - }, - "hideAt": { - "format": "date-time", - "type": "string" - }, "id": { "type": "string" }, @@ -12462,14 +12490,6 @@ "ownerId": { "type": "string" }, - "seenAt": { - "format": "date-time", - "type": "string" - }, - "showAt": { - "format": "date-time", - "type": "string" - }, "type": { "allOf": [ { @@ -12495,6 +12515,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 5c952c30af..469dc26580 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -758,14 +758,10 @@ export type MemoryResponseDto = { assets: AssetResponseDto[]; createdAt: string; data: OnThisDayDto; - deletedAt?: string; - hideAt?: string; id: string; isSaved: boolean; memoryAt: string; ownerId: string; - seenAt?: string; - showAt?: string; "type": MemoryType; updatedAt: string; }; @@ -2863,10 +2859,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<{ @@ -2876,6 +2874,8 @@ export function searchMemories({ $for, isSaved, isTrashed, $type }: { "for": $for, isSaved, isTrashed, + order, + size, "type": $type }))}`, { ...opts @@ -2899,10 +2899,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<{ @@ -2912,6 +2914,8 @@ export function memoriesStatistics({ $for, isSaved, isTrashed, $type }: { "for": $for, isSaved, isTrashed, + order, + size, "type": $type }))}`, { ...opts @@ -4891,6 +4895,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 a79511c73e..8663cd2c0b 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, default: AssetOrderWithRandom.Desc }) + order?: AssetOrderWithRandom; } class OnThisDayDto { diff --git a/server/src/enum.ts b/server/src/enum.ts index b8e6e5209f..abcfd90234 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 65b4cb3df7..0609f67637 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(); }