mirror of
https://github.com/immich-app/immich.git
synced 2025-06-23 15:30:51 -04:00
feat(server): add memories statistics resource (#19035)
This commit is contained in:
parent
16745e77d4
commit
e88bd74fd2
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@ -144,6 +144,7 @@ Class | Method | HTTP request | Description
|
||||
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |
|
||||
*MemoriesApi* | [**deleteMemory**](doc//MemoriesApi.md#deletememory) | **DELETE** /memories/{id} |
|
||||
*MemoriesApi* | [**getMemory**](doc//MemoriesApi.md#getmemory) | **GET** /memories/{id} |
|
||||
*MemoriesApi* | [**memoriesStatistics**](doc//MemoriesApi.md#memoriesstatistics) | **GET** /memories/statistics |
|
||||
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
||||
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
||||
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
||||
@ -375,6 +376,7 @@ Class | Method | HTTP request | Description
|
||||
- [MemoriesUpdate](doc//MemoriesUpdate.md)
|
||||
- [MemoryCreateDto](doc//MemoryCreateDto.md)
|
||||
- [MemoryResponseDto](doc//MemoryResponseDto.md)
|
||||
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
|
||||
- [MemoryType](doc//MemoryType.md)
|
||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||
- [MergePersonDto](doc//MergePersonDto.md)
|
||||
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -163,6 +163,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_statistics_response_dto.dart';
|
||||
part 'model/memory_type.dart';
|
||||
part 'model/memory_update_dto.dart';
|
||||
part 'model/merge_person_dto.dart';
|
||||
|
72
mobile/openapi/lib/api/memories_api.dart
generated
72
mobile/openapi/lib/api/memories_api.dart
generated
@ -206,6 +206,78 @@ class MemoriesApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /memories/statistics' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] for_:
|
||||
///
|
||||
/// * [bool] isSaved:
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
Future<Response> memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemoryType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/memories/statistics';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (for_ != null) {
|
||||
queryParams.addAll(_queryParams('', 'for', for_));
|
||||
}
|
||||
if (isSaved != null) {
|
||||
queryParams.addAll(_queryParams('', 'isSaved', isSaved));
|
||||
}
|
||||
if (isTrashed != null) {
|
||||
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
|
||||
}
|
||||
if (type != null) {
|
||||
queryParams.addAll(_queryParams('', 'type', type));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] for_:
|
||||
///
|
||||
/// * [bool] isSaved:
|
||||
///
|
||||
/// * [bool] isTrashed:
|
||||
///
|
||||
/// * [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, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryStatisticsResponseDto',) as MemoryStatisticsResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /memories/{id}/assets' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@ -382,6 +382,8 @@ class ApiClient {
|
||||
return MemoryCreateDto.fromJson(value);
|
||||
case 'MemoryResponseDto':
|
||||
return MemoryResponseDto.fromJson(value);
|
||||
case 'MemoryStatisticsResponseDto':
|
||||
return MemoryStatisticsResponseDto.fromJson(value);
|
||||
case 'MemoryType':
|
||||
return MemoryTypeTypeTransformer().decode(value);
|
||||
case 'MemoryUpdateDto':
|
||||
|
99
mobile/openapi/lib/model/memory_statistics_response_dto.dart
generated
Normal file
99
mobile/openapi/lib/model/memory_statistics_response_dto.dart
generated
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// 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 MemoryStatisticsResponseDto {
|
||||
/// Returns a new [MemoryStatisticsResponseDto] instance.
|
||||
MemoryStatisticsResponseDto({
|
||||
required this.total,
|
||||
});
|
||||
|
||||
int total;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MemoryStatisticsResponseDto &&
|
||||
other.total == total;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(total.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MemoryStatisticsResponseDto[total=$total]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'total'] = this.total;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MemoryStatisticsResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MemoryStatisticsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MemoryStatisticsResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MemoryStatisticsResponseDto(
|
||||
total: mapValueOfType<int>(json, r'total')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MemoryStatisticsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MemoryStatisticsResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MemoryStatisticsResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MemoryStatisticsResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MemoryStatisticsResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MemoryStatisticsResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MemoryStatisticsResponseDto-objects as value to a dart map
|
||||
static Map<String, List<MemoryStatisticsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MemoryStatisticsResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MemoryStatisticsResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'total',
|
||||
};
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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<MemoryStatisticsResponseDto> {
|
||||
return this.service.statistics(auth, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.MEMORY_READ })
|
||||
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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<number>().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();
|
||||
}
|
||||
|
@ -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<MemoryResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] });
|
||||
const memory = await this.findOrFail(id);
|
||||
|
Loading…
x
Reference in New Issue
Block a user