import { BadRequestException, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; 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, MemorySearchDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { addAssets, getMyPartnerIds, removeAssets } from 'src/utils/asset.util'; const DAYS = 3; @Injectable() export class MemoryService extends BaseService { @OnJob({ name: JobName.MEMORIES_CREATE, queue: QueueName.BACKGROUND_TASK }) async onMemoriesCreate() { const users = await this.userRepository.getList({ withDeleted: false }); const usersIds = await Promise.all( users.map((user) => getMyPartnerIds({ userId: user.id, repository: this.partnerRepository, timelineEnabled: true, }), ), ); await this.databaseRepository.withLock(DatabaseLock.MemoryCreation, async () => { const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE); const start = DateTime.utc().startOf('day').minus({ days: DAYS }); const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start; // generate a memory +/- X days from today for (let i = 0; i <= DAYS * 2; i++) { const target = start.plus({ days: i }); if (lastOnThisDayDate >= target) { continue; } try { await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target))); } catch (error) { this.logger.error(`Failed to create memories for ${target.toISO()}`, error); } // update system metadata even when there is an error to minimize the chance of duplicates await this.systemMetadataRepository.set(SystemMetadataKey.MEMORIES_STATE, { ...state, lastOnThisDayDate: target.toISO(), }); } }); } private async createOnThisDayMemories(ownerId: string, userIds: string[], target: DateTime) { const showAt = target.startOf('day').toISO(); const hideAt = target.endOf('day').toISO(); const memories = await this.assetRepository.getByDayOfYear([ownerId, ...userIds], target); await Promise.all( memories.map(({ year, assets }) => this.memoryRepository.create( { ownerId, type: MemoryType.ON_THIS_DAY, data: { year }, memoryAt: target.set({ year }).toISO()!, showAt, hideAt, }, new Set(assets.map(({ id }) => id)), ), ), ); } @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, auth)); } async get(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id); return mapMemory(memory, auth); } async create(auth: AuthDto, dto: MemoryCreateDto) { // TODO validate type/data combination const assetIds = dto.assetIds || []; const allowedAssetIds = await this.checkAccess({ auth, permission: Permission.ASSET_SHARE, ids: assetIds, }); const memory = await this.memoryRepository.create( { ownerId: auth.user.id, type: dto.type, data: dto.data, isSaved: dto.isSaved, memoryAt: dto.memoryAt, seenAt: dto.seenAt, }, allowedAssetIds, ); return mapMemory(memory, auth); } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); const memory = await this.memoryRepository.update(id, { isSaved: dto.isSaved, memoryAt: dto.memoryAt, seenAt: dto.seenAt, }); return mapMemory(memory, auth); } async remove(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_DELETE, ids: [id] }); await this.memoryRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_READ, ids: [id] }); const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { await this.memoryRepository.update(id, { updatedAt: new Date() }); } return results; } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await removeAssets(auth, repos, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.MEMORY_DELETE, }); const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { await this.memoryRepository.update(id, { id, updatedAt: new Date() }); } return results; } private async findOrFail(id: string) { const memory = await this.memoryRepository.get(id); if (!memory) { throw new BadRequestException('Memory not found'); } return memory; } }