feat: persistent memories (#15953)

feat: memories

refactor

chore: use heart as favorite icon

fix: linting
This commit is contained in:
Jason Rasmussen
2025-02-21 13:31:37 -05:00
committed by GitHub
parent 502f6e020d
commit d350022dec
29 changed files with 585 additions and 70 deletions

View File

@@ -40,6 +40,8 @@ describe(JobService.name, () => {
{ name: JobName.ASSET_DELETION_CHECK },
{ name: JobName.USER_DELETE_CHECK },
{ name: JobName.PERSON_CLEANUP },
{ name: JobName.MEMORIES_CLEANUP },
{ name: JobName.MEMORIES_CREATE },
{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
{ name: JobName.CLEAN_OLD_AUDIT_LOGS },
{ name: JobName.USER_SYNC_USAGE },

View File

@@ -31,6 +31,14 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
return { name: JobName.USER_DELETE_CHECK };
}
case ManualJobName.MEMORY_CLEANUP: {
return { name: JobName.MEMORIES_CLEANUP };
}
case ManualJobName.MEMORY_CREATE: {
return { name: JobName.MEMORIES_CREATE };
}
default: {
throw new BadRequestException('Invalid job name');
}
@@ -207,6 +215,8 @@ export class JobService extends BaseService {
{ name: JobName.ASSET_DELETION_CHECK },
{ name: JobName.USER_DELETE_CHECK },
{ name: JobName.PERSON_CLEANUP },
{ name: JobName.MEMORIES_CLEANUP },
{ name: JobName.MEMORIES_CREATE },
{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
{ name: JobName.CLEAN_OLD_AUDIT_LOGS },
{ name: JobName.USER_SYNC_USAGE },

View File

@@ -21,7 +21,7 @@ describe(MemoryService.name, () => {
describe('search', () => {
it('should search memories', async () => {
mocks.memory.search.mockResolvedValue([memoryStub.memory1, memoryStub.empty]);
await expect(sut.search(authStub.admin)).resolves.toEqual(
await expect(sut.search(authStub.admin, {})).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'memory1', assets: expect.any(Array) }),
expect.objectContaining({ id: 'memoryEmpty', assets: [] }),
@@ -30,7 +30,7 @@ describe(MemoryService.name, () => {
});
it('should map ', async () => {
await expect(sut.search(authStub.admin)).resolves.toEqual([]);
await expect(sut.search(authStub.admin, {})).resolves.toEqual([]);
});
});

View File

@@ -1,16 +1,84 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { JsonObject } from 'src/db';
import { OnJob } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
import { Permission } from 'src/enum';
import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
import { OnThisDayData } from 'src/entities/memory.entity';
import { JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util';
import { addAssets, getMyPartnerIds, removeAssets } from 'src/utils/asset.util';
const DAYS = 3;
@Injectable()
export class MemoryService extends BaseService {
async search(auth: AuthDto) {
const memories = await this.memoryRepository.search(auth.user.id);
@OnJob({ name: JobName.MEMORIES_CREATE, queue: QueueName.BACKGROUND_TASK })
async onMemoriesCreate() {
const users = await this.userRepository.getList({ withDeleted: false });
const userMap: Record<string, string[]> = {};
for (const user of users) {
const partnerIds = await getMyPartnerIds({
userId: user.id,
repository: this.partnerRepository,
timelineEnabled: true,
});
userMap[user.id] = [user.id, ...partnerIds];
}
const start = DateTime.utc().startOf('day').minus({ days: DAYS });
const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE);
let lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state?.lastOnThisDayDate) : start;
// generate a memory +/- X days from today
for (let i = 0; i <= DAYS * 2 + 1; i++) {
const target = start.plus({ days: i });
if (lastOnThisDayDate > target) {
continue;
}
const showAt = target.startOf('day').toISO();
const hideAt = target.endOf('day').toISO();
this.logger.log(`Creating memories for month=${target.month}, day=${target.day}`);
for (const [userId, userIds] of Object.entries(userMap)) {
const memories = await this.assetRepository.getByDayOfYear(userIds, target);
for (const memory of memories) {
const data: OnThisDayData = { year: target.year - memory.yearsAgo };
await this.memoryRepository.create(
{
ownerId: userId,
type: MemoryType.ON_THIS_DAY,
data,
memoryAt: target.minus({ years: memory.yearsAgo }).toISO(),
showAt,
hideAt,
},
new Set(memory.assets.map(({ id }) => id)),
);
}
}
await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, {
...state,
lastOnThisDayDate: target.toISO(),
});
lastOnThisDayDate = target;
}
}
@OnJob({ name: JobName.MEMORIES_CLEANUP, queue: QueueName.BACKGROUND_TASK })
async onMemoriesCleanup() {
await this.memoryRepository.cleanup();
}
async search(auth: AuthDto, dto: MemorySearchDto) {
const memories = await this.memoryRepository.search(auth.user.id, dto);
return memories.map((memory) => mapMemory(memory));
}