import { Injectable } from '@nestjs/common'; 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 { AssetOrderWithRandom, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; import { MemoryTable } from 'src/schema/tables/memory.table'; import { IBulkAsset } from 'src/types'; @Injectable() export class MemoryRepository implements IBulkAsset { constructor(@InjectKysely() private db: Kysely) {} async cleanup() { await this.db .deleteFrom('memory_asset') .using('asset') .whereRef('memory_asset.assetId', '=', 'asset.id') .where('asset.visibility', '!=', AssetVisibility.Timeline) .execute(); return this.db .deleteFrom('memory') .where('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()) .where('isSaved', '=', false) .execute(); } searchBuilder(ownerId: string, dto: MemorySearchDto) { return this.db .selectFrom('memory') .$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().as('total')) .executeTakeFirstOrThrow(); } @GenerateSql( { params: [DummyValue.UUID, {}] }, { name: 'date filter', params: [DummyValue.UUID, { for: DummyValue.DATE }] }, ) search(ownerId: string, dto: MemorySearchDto) { return this.searchBuilder(ownerId, dto) .select((eb) => jsonArrayFrom( eb .selectFrom('asset') .selectAll('asset') .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetId') .whereRef('memory_asset.memoriesId', '=', 'memory.id') .orderBy('asset.fileCreatedAt', 'asc') .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .where('asset.deletedAt', 'is', null), ).as('assets'), ) .selectAll('memory') .$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(); } @GenerateSql({ params: [DummyValue.UUID] }) get(id: string) { return this.getByIdBuilder(id).executeTakeFirst(); } async create(memory: Insertable, assetIds: Set) { const id = await this.db.transaction().execute(async (tx) => { const { id } = await tx.insertInto('memory').values(memory).returning('id').executeTakeFirstOrThrow(); if (assetIds.size > 0) { const values = [...assetIds].map((assetId) => ({ memoriesId: id, assetId })); await tx.insertInto('memory_asset').values(values).execute(); } return id; }); return this.getByIdBuilder(id).executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] }) async update(id: string, memory: Updateable) { await this.db.updateTable('memory').set(memory).where('id', '=', id).execute(); return this.getByIdBuilder(id).executeTakeFirstOrThrow(); } @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string) { await this.db.deleteFrom('memory').where('id', '=', id).execute(); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @ChunkedSet({ paramIndex: 1 }) async getAssetIds(id: string, assetIds: string[]) { if (assetIds.length === 0) { return new Set(); } const results = await this.db .selectFrom('memory_asset') .select(['assetId']) .where('memoriesId', '=', id) .where('assetId', 'in', assetIds) .execute(); return new Set(results.map(({ assetId }) => assetId)); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async addAssetIds(id: string, assetIds: string[]) { if (assetIds.length === 0) { return; } await this.db .insertInto('memory_asset') .values(assetIds.map((assetId) => ({ memoriesId: id, assetId }))) .execute(); } @Chunked({ paramIndex: 1 }) @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async removeAssetIds(id: string, assetIds: string[]) { if (assetIds.length === 0) { return; } await this.db.deleteFrom('memory_asset').where('memoriesId', '=', id).where('assetId', 'in', assetIds).execute(); } private getByIdBuilder(id: string) { return this.db .selectFrom('memory') .selectAll('memory') .select((eb) => jsonArrayFrom( eb .selectFrom('asset') .selectAll('asset') .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetId') .whereRef('memory_asset.memoriesId', '=', 'memory.id') .orderBy('asset.fileCreatedAt', 'asc') .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .where('asset.deletedAt', 'is', null), ).as('assets'), ) .where('id', '=', id) .where('deletedAt', 'is', null); } }