diff --git a/server/src/interfaces/memory.interface.ts b/server/src/interfaces/memory.interface.ts index 308943d97e..b1dbcbef85 100644 --- a/server/src/interfaces/memory.interface.ts +++ b/server/src/interfaces/memory.interface.ts @@ -1,4 +1,6 @@ -import { MemoryEntity } from 'src/entities/memory.entity'; +import { Insertable, Updateable } from 'kysely'; +import { Memories } from 'src/db'; +import { MemoryEntity, OnThisDayData } from 'src/entities/memory.entity'; import { IBulkAsset } from 'src/utils/asset.util'; export const IMemoryRepository = 'IMemoryRepository'; @@ -6,7 +8,10 @@ export const IMemoryRepository = 'IMemoryRepository'; export interface IMemoryRepository extends IBulkAsset { search(ownerId: string): Promise; get(id: string): Promise; - create(memory: Partial): Promise; - update(memory: Partial): Promise; + create( + memory: Omit, 'data'> & { data: OnThisDayData }, + assetIds: Set, + ): Promise; + update(id: string, memory: Updateable): Promise; delete(id: string): Promise; } diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index e3945ca028..396da3f56e 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -1,10 +1,79 @@ -- NOTE: This file is auto generated by ./sql-generator +-- MemoryRepository.search +select + * +from + "memories" +where + "ownerId" = $1 +order by + "memoryAt" desc + +-- MemoryRepository.get +select + "memories".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "assets".* + from + "assets" + inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + where + "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."deletedAt" is null + ) as agg + ) as "assets" +from + "memories" +where + "id" = $1 + and "deletedAt" is null + +-- MemoryRepository.update +update "memories" +set + "ownerId" = $1, + "isSaved" = $2 +where + "id" = $3 +select + "memories".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "assets".* + from + "assets" + inner join "memories_assets_assets" on "assets"."id" = "memories_assets_assets"."assetsId" + where + "memories_assets_assets"."memoriesId" = "memories"."id" + and "assets"."deletedAt" is null + ) as agg + ) as "assets" +from + "memories" +where + "id" = $1 + and "deletedAt" is null + +-- MemoryRepository.delete +delete from "memories" +where + "id" = $1 + -- MemoryRepository.getAssetIds -SELECT - "memories_assets"."assetsId" AS "assetId" -FROM - "memories_assets_assets" "memories_assets" -WHERE - "memories_assets"."memoriesId" = $1 - AND "memories_assets"."assetsId" IN ($2) +select + "assetsId" +from + "memories_assets_assets" +where + "memoriesId" = $1 + and "assetsId" in ($2) diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 47dc705093..7e59b92e68 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -1,49 +1,55 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, Updateable } from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Memories } from 'src/db'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { MemoryEntity } from 'src/entities/memory.entity'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; -import { DataSource, In, Repository } from 'typeorm'; @Injectable() export class MemoryRepository implements IMemoryRepository { - constructor( - @InjectRepository(MemoryEntity) private repository: Repository, - @InjectDataSource() private dataSource: DataSource, - ) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: [DummyValue.UUID] }) search(ownerId: string): Promise { - return this.repository.find({ - where: { - ownerId, - }, - order: { - memoryAt: 'DESC', - }, - }); + return this.db + .selectFrom('memories') + .selectAll() + .where('ownerId', '=', ownerId) + .orderBy('memoryAt', 'desc') + .execute() as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) get(id: string): Promise { - return this.repository.findOne({ - where: { - id, - }, - relations: { - assets: true, - }, + return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise; + } + + async create(memory: Insertable, assetIds: Set): Promise { + const id = await this.db.transaction().execute(async (tx) => { + const { id } = await tx.insertInto('memories').values(memory).returning('id').executeTakeFirstOrThrow(); + + if (assetIds.size > 0) { + const values = [...assetIds].map((assetId) => ({ memoriesId: id, assetsId: assetId })); + await tx.insertInto('memories_assets_assets').values(values).execute(); + } + + return id; }); + + return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise; } - create(memory: Partial): Promise { - return this.save(memory); - } - - update(memory: Partial): Promise { - return this.save(memory); + @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] }) + async update(id: string, memory: Updateable): Promise { + await this.db.updateTable('memories').set(memory).where('id', '=', id).execute(); + return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise; } + @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { - await this.repository.delete({ id }); + await this.db.deleteFrom('memories').where('id', '=', id).execute(); } @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @@ -53,46 +59,49 @@ export class MemoryRepository implements IMemoryRepository { return new Set(); } - const results = await this.dataSource - .createQueryBuilder() - .select('memories_assets.assetsId', 'assetId') - .from('memories_assets_assets', 'memories_assets') - .where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id }) - .andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds }) - .getRawMany<{ assetId: string }>(); + const results = await this.db + .selectFrom('memories_assets_assets') + .select(['assetsId']) + .where('memoriesId', '=', id) + .where('assetsId', 'in', assetIds) + .execute(); - return new Set(results.map(({ assetId }) => assetId)); + return new Set(results.map(({ assetsId }) => assetsId)); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async addAssetIds(id: string, assetIds: string[]): Promise { - await this.dataSource - .createQueryBuilder() - .insert() - .into('memories_assets_assets', ['memoriesId', 'assetsId']) + await this.db + .insertInto('memories_assets_assets') .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) .execute(); } @Chunked({ paramIndex: 1 }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async removeAssetIds(id: string, assetIds: string[]): Promise { - await this.dataSource - .createQueryBuilder() - .delete() - .from('memories_assets_assets') - .where({ - memoriesId: id, - assetsId: In(assetIds), - }) + await this.db + .deleteFrom('memories_assets_assets') + .where('memoriesId', '=', id) + .where('assetsId', 'in', assetIds) .execute(); } - private async save(memory: Partial): Promise { - const { id } = await this.repository.save(memory); - return this.repository.findOneOrFail({ - where: { id }, - relations: { - assets: true, - }, - }); + private getByIdBuilder(id: string) { + return this.db + .selectFrom('memories') + .selectAll('memories') + .select((eb) => + jsonArrayFrom( + eb + .selectFrom('assets') + .selectAll('assets') + .innerJoin('memories_assets_assets', 'assets.id', 'memories_assets_assets.assetsId') + .whereRef('memories_assets_assets.memoriesId', '=', 'memories.id') + .where('assets.deletedAt', 'is', null), + ).as('assets'), + ) + .where('id', '=', id) + .where('deletedAt', 'is', null); } } diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index b5dd4c2553..9c5336eb6e 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -69,7 +69,17 @@ describe(MemoryService.name, () => { memoryAt: new Date(2024), }), ).resolves.toMatchObject({ assets: [] }); - expect(memoryMock.create).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); + expect(memoryMock.create).toHaveBeenCalledWith( + { + ownerId: 'admin_id', + memoryAt: expect.any(Date), + type: MemoryType.ON_THIS_DAY, + isSaved: undefined, + sendAt: undefined, + data: { year: 2024 }, + }, + new Set(), + ); }); it('should create a memory', async () => { @@ -80,14 +90,14 @@ describe(MemoryService.name, () => { type: MemoryType.ON_THIS_DAY, data: { year: 2024 }, assetIds: ['asset1'], - memoryAt: new Date(2024), + memoryAt: new Date(2024, 0, 1), }), ).resolves.toBeDefined(); expect(memoryMock.create).toHaveBeenCalledWith( expect.objectContaining({ ownerId: userStub.admin.id, - assets: [{ id: 'asset1' }], }), + new Set(['asset1']), ); }); @@ -115,12 +125,7 @@ describe(MemoryService.name, () => { accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); memoryMock.update.mockResolvedValue(memoryStub.memory1); await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined(); - expect(memoryMock.update).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'memory1', - isSaved: true, - }), - ); + expect(memoryMock.update).toHaveBeenCalledWith('memory1', expect.objectContaining({ isSaved: true })); }); }); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 816b0fddeb..926571e43c 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; 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 { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @@ -29,15 +28,17 @@ export class MemoryService extends BaseService { 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, - assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity), - }); + 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); } @@ -45,8 +46,7 @@ export class MemoryService extends BaseService { 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, + const memory = await this.memoryRepository.update(id, { isSaved: dto.isSaved, memoryAt: dto.memoryAt, seenAt: dto.seenAt, @@ -68,7 +68,7 @@ export class MemoryService extends BaseService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.memoryRepository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update(id, { updatedAt: new Date() }); } return results; @@ -86,7 +86,7 @@ export class MemoryService extends BaseService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.memoryRepository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update(id, { id, updatedAt: new Date() }); } return results;