feat: random memories sort order (#20025)

This commit is contained in:
Jonathan Gilbert 2025-11-11 01:38:50 +11:00 committed by GitHub
parent 6c6b00067b
commit d92df63f84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 218 additions and 12 deletions

View file

@ -407,6 +407,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)

View file

@ -175,6 +175,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';

View file

@ -238,8 +238,13 @@ class MemoriesApi {
///
/// * [bool] isTrashed:
///
/// * [MemorySearchOrder] order:
///
/// * [int] size:
/// Number of memories to return
///
/// * [MemoryType] type:
Future<Response> memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async {
Future<Response> 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<MemoryStatisticsResponseDto?> memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async {
final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, type: type, );
Future<MemoryStatisticsResponseDto?> 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<Response> searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async {
Future<Response> 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<List<MemoryResponseDto>?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async {
final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, type: type, );
Future<List<MemoryResponseDto>?> 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));
}

View file

@ -404,6 +404,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':

View file

@ -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();
}

View file

@ -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 = <MemorySearchOrder>[
asc,
desc,
random,
];
static MemorySearchOrder? fromJson(dynamic value) => MemorySearchOrderTypeTransformer().decode(value);
static List<MemorySearchOrder> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MemorySearchOrder>[];
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;
}

View file

@ -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": {

View file

@ -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"
}

View file

@ -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 {

View file

@ -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',

View file

@ -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();
}