mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
fix(server): memory lane title (#2772)
* fix(server): memory lane title * feat: parallel requests * pr feedback * fix test --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
045bb855d2
commit
896645130b
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
@ -1539,10 +1539,12 @@
|
|||||||
"operationId": "getMemoryLane",
|
"operationId": "getMemoryLane",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timestamp",
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "query",
|
"in": "query",
|
||||||
|
"description": "Get pictures for +24 hours from this time going back x years",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { assetEntityStub, authStub, newAssetRepositoryMock } from '@test';
|
import { assetEntityStub, authStub, newAssetRepositoryMock } from '@test';
|
||||||
import { AssetService, IAssetRepository } from '.';
|
import { when } from 'jest-when';
|
||||||
|
import { AssetService, IAssetRepository, mapAsset } from '.';
|
||||||
|
|
||||||
describe(AssetService.name, () => {
|
describe(AssetService.name, () => {
|
||||||
let sut: AssetService;
|
let sut: AssetService;
|
||||||
@ -38,4 +39,62 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMemoryLane', () => {
|
||||||
|
it('should get pictures for each year', async () => {
|
||||||
|
assetMock.getByDate.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 10 })).resolves.toEqual(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(assetMock.getByDate).toHaveBeenCalledTimes(10);
|
||||||
|
expect(assetMock.getByDate.mock.calls).toEqual([
|
||||||
|
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2020-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2019-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2018-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2017-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2016-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2015-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2014-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2013-06-15T00:00:00.000Z')],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep hours from the date', async () => {
|
||||||
|
assetMock.getByDate.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15, 5), years: 2 }),
|
||||||
|
).resolves.toEqual([]);
|
||||||
|
|
||||||
|
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
|
||||||
|
expect(assetMock.getByDate.mock.calls).toEqual([
|
||||||
|
[authStub.admin.id, new Date('2022-06-15T05:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2021-06-15T05:00:00.000Z')],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the title correctly', async () => {
|
||||||
|
when(assetMock.getByDate)
|
||||||
|
.calledWith(authStub.admin.id, new Date('2022-06-15T00:00:00.000Z'))
|
||||||
|
.mockResolvedValue([assetEntityStub.image]);
|
||||||
|
when(assetMock.getByDate)
|
||||||
|
.calledWith(authStub.admin.id, new Date('2021-06-15T00:00:00.000Z'))
|
||||||
|
.mockResolvedValue([assetEntityStub.video]);
|
||||||
|
|
||||||
|
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 2 })).resolves.toEqual([
|
||||||
|
{ title: '1 year since...', assets: [mapAsset(assetEntityStub.image)] },
|
||||||
|
{ title: '2 years since...', assets: [mapAsset(assetEntityStub.video)] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
|
||||||
|
expect(assetMock.getByDate.mock.calls).toEqual([
|
||||||
|
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
|
||||||
|
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Inject } from '@nestjs/common';
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { IAssetRepository } from './asset.repository';
|
import { IAssetRepository } from './asset.repository';
|
||||||
|
import { MemoryLaneDto } from './dto';
|
||||||
import { MapMarkerDto } from './dto/map-marker.dto';
|
import { MapMarkerDto } from './dto/map-marker.dto';
|
||||||
import { MapMarkerResponseDto, mapAsset } from './response-dto';
|
import { mapAsset, MapMarkerResponseDto } from './response-dto';
|
||||||
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
|
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
|
constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
|
||||||
@ -13,30 +14,22 @@ export class AssetService {
|
|||||||
return this.assetRepository.getMapMarkers(authUser.id, options);
|
return this.assetRepository.getMapMarkers(authUser.id, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMemoryLane(authUser: AuthUserDto, timezone: string): Promise<MemoryLaneResponseDto[]> {
|
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||||
const result: MemoryLaneResponseDto[] = [];
|
const target = DateTime.fromJSDate(dto.timestamp);
|
||||||
|
|
||||||
const luxonDate = DateTime.fromISO(new Date().toISOString(), { zone: timezone });
|
const onRequest = async (yearsAgo: number): Promise<MemoryLaneResponseDto> => {
|
||||||
const today = new Date(luxonDate.year, luxonDate.month - 1, luxonDate.day);
|
const assets = await this.assetRepository.getByDate(authUser.id, target.minus({ years: yearsAgo }).toJSDate());
|
||||||
|
return {
|
||||||
|
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
|
||||||
|
assets: assets.map((a) => mapAsset(a)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const years = Array.from({ length: 30 }, (_, i) => {
|
const requests: Promise<MemoryLaneResponseDto>[] = [];
|
||||||
const year = today.getFullYear() - i - 1;
|
for (let i = 1; i <= dto.years; i++) {
|
||||||
return new Date(year, today.getMonth(), today.getDate());
|
requests.push(onRequest(i));
|
||||||
});
|
|
||||||
|
|
||||||
for (const year of years) {
|
|
||||||
const assets = await this.assetRepository.getByDate(authUser.id, year);
|
|
||||||
|
|
||||||
if (assets.length > 0) {
|
|
||||||
const yearGap = today.getFullYear() - year.getFullYear();
|
|
||||||
const memory = new MemoryLaneResponseDto();
|
|
||||||
memory.title = `${yearGap} year${yearGap > 1 && 's'} since...`;
|
|
||||||
memory.assets = assets.map((a) => mapAsset(a));
|
|
||||||
|
|
||||||
result.push(memory);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './asset-ids.dto';
|
export * from './asset-ids.dto';
|
||||||
export * from './map-marker.dto';
|
export * from './map-marker.dto';
|
||||||
|
export * from './memory-lane.dto';
|
||||||
|
14
server/src/domain/asset/dto/memory-lane.dto.ts
Normal file
14
server/src/domain/asset/dto/memory-lane.dto.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsDate, IsNumber, IsPositive } from 'class-validator';
|
||||||
|
|
||||||
|
export class MemoryLaneDto {
|
||||||
|
/** Get pictures for +24 hours from this time going back x years */
|
||||||
|
@IsDate()
|
||||||
|
@Type(() => Date)
|
||||||
|
timestamp!: Date;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsPositive()
|
||||||
|
@Type(() => Number)
|
||||||
|
years = 30;
|
||||||
|
}
|
@ -2,6 +2,5 @@ import { AssetResponseDto } from './asset-response.dto';
|
|||||||
|
|
||||||
export class MemoryLaneResponseDto {
|
export class MemoryLaneResponseDto {
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
assets!: AssetResponseDto[];
|
assets!: AssetResponseDto[];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AssetService, AuthUserDto, MapMarkerResponseDto } from '@app/domain';
|
import { AssetService, AuthUserDto, MapMarkerResponseDto, MemoryLaneDto } from '@app/domain';
|
||||||
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
|
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
|
||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
@ -20,10 +20,7 @@ export class AssetController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('memory-lane')
|
@Get('memory-lane')
|
||||||
getMemoryLane(
|
getMemoryLane(@GetAuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
return this.service.getMemoryLane(authUser, dto);
|
||||||
@Query('timezone') timezone: string,
|
|
||||||
): Promise<MemoryLaneResponseDto[]> {
|
|
||||||
return this.service.getMemoryLane(authUser, timezone);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
|
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
|
||||||
import { AssetEntity, AssetType } from '../entities';
|
import { AssetEntity, AssetType } from '../entities';
|
||||||
import OptionalBetween from '../utils/optional-between.util';
|
import OptionalBetween from '../utils/optional-between.util';
|
||||||
@ -21,7 +22,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
|
constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
|
||||||
|
|
||||||
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
|
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
|
||||||
// For reference of a correct approach althought slower
|
// For reference of a correct approach although slower
|
||||||
|
|
||||||
// let builder = this.repository
|
// let builder = this.repository
|
||||||
// .createQueryBuilder('asset')
|
// .createQueryBuilder('asset')
|
||||||
@ -36,14 +37,13 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
// .orderBy('asset.fileCreatedAt', 'DESC');
|
// .orderBy('asset.fileCreatedAt', 'DESC');
|
||||||
|
|
||||||
// return builder.getMany();
|
// return builder.getMany();
|
||||||
const tomorrow = new Date(date.getTime() + 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
return this.repository.find({
|
return this.repository.find({
|
||||||
where: {
|
where: {
|
||||||
ownerId,
|
ownerId,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
fileCreatedAt: OptionalBetween(date, tomorrow),
|
fileCreatedAt: OptionalBetween(date, DateTime.fromJSDate(date).plus({ day: 1 }).toJSDate()),
|
||||||
},
|
},
|
||||||
relations: {
|
relations: {
|
||||||
exifInfo: true,
|
exifInfo: true,
|
||||||
|
32
web/src/api/open-api/api.ts
generated
32
web/src/api/open-api/api.ts
generated
@ -5523,13 +5523,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} timezone
|
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMemoryLane: async (timezone: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'timezone' is not null or undefined
|
// verify required parameter 'timestamp' is not null or undefined
|
||||||
assertParamExists('getMemoryLane', 'timezone', timezone)
|
assertParamExists('getMemoryLane', 'timestamp', timestamp)
|
||||||
const localVarPath = `/asset/memory-lane`;
|
const localVarPath = `/asset/memory-lane`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -5551,8 +5551,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
if (timezone !== undefined) {
|
if (timestamp !== undefined) {
|
||||||
localVarQueryParameter['timezone'] = timezone;
|
localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ?
|
||||||
|
(timestamp as any).toISOString() :
|
||||||
|
timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -6157,12 +6159,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} timezone
|
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getMemoryLane(timezone: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timezone, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -6446,12 +6448,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} timezone
|
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMemoryLane(timezone: string, options?: any): AxiosPromise<Array<MemoryLaneResponseDto>> {
|
getMemoryLane(timestamp: string, options?: any): AxiosPromise<Array<MemoryLaneResponseDto>> {
|
||||||
return localVarFp.getMemoryLane(timezone, options).then((request) => request(axios, basePath));
|
return localVarFp.getMemoryLane(timestamp, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
@ -6857,11 +6859,11 @@ export interface AssetApiGetMapMarkersRequest {
|
|||||||
*/
|
*/
|
||||||
export interface AssetApiGetMemoryLaneRequest {
|
export interface AssetApiGetMemoryLaneRequest {
|
||||||
/**
|
/**
|
||||||
*
|
* Get pictures for +24 hours from this time going back x years
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AssetApiGetMemoryLane
|
* @memberof AssetApiGetMemoryLane
|
||||||
*/
|
*/
|
||||||
readonly timezone: string
|
readonly timestamp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7304,7 +7306,7 @@ export class AssetApi extends BaseAPI {
|
|||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) {
|
public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timezone, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,8 +35,9 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!$memoryStore) {
|
if (!$memoryStore) {
|
||||||
const timezone = DateTime.local().zoneName;
|
const { data } = await api.assetApi.getMemoryLane({
|
||||||
const { data } = await api.assetApi.getMemoryLane({ timezone });
|
timestamp: DateTime.local().startOf('day').toISO()
|
||||||
|
});
|
||||||
$memoryStore = data;
|
$memoryStore = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,8 +11,9 @@
|
|||||||
$: shouldRender = memoryLane.length > 0;
|
$: shouldRender = memoryLane.length > 0;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const timezone = DateTime.local().zoneName;
|
const { data } = await api.assetApi.getMemoryLane({
|
||||||
const { data } = await api.assetApi.getMemoryLane({ timezone });
|
timestamp: DateTime.local().startOf('day').toISO()
|
||||||
|
});
|
||||||
|
|
||||||
memoryLane = data;
|
memoryLane = data;
|
||||||
$memoryStore = data;
|
$memoryStore = data;
|
||||||
|
Loading…
Reference in New Issue
Block a user