refactor: migrate memory to kysely (#15314)

This commit is contained in:
Jason Rasmussen 2025-01-15 11:34:11 -05:00 committed by GitHub
parent 43b3181f45
commit 93e2545275
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 177 additions and 89 deletions

View File

@ -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'; import { IBulkAsset } from 'src/utils/asset.util';
export const IMemoryRepository = 'IMemoryRepository'; export const IMemoryRepository = 'IMemoryRepository';
@ -6,7 +8,10 @@ export const IMemoryRepository = 'IMemoryRepository';
export interface IMemoryRepository extends IBulkAsset { export interface IMemoryRepository extends IBulkAsset {
search(ownerId: string): Promise<MemoryEntity[]>; search(ownerId: string): Promise<MemoryEntity[]>;
get(id: string): Promise<MemoryEntity | null>; get(id: string): Promise<MemoryEntity | null>;
create(memory: Partial<MemoryEntity>): Promise<MemoryEntity>; create(
update(memory: Partial<MemoryEntity>): Promise<MemoryEntity>; memory: Omit<Insertable<Memories>, 'data'> & { data: OnThisDayData },
assetIds: Set<string>,
): Promise<MemoryEntity>;
update(id: string, memory: Updateable<Memories>): Promise<MemoryEntity>;
delete(id: string): Promise<void>; delete(id: string): Promise<void>;
} }

View File

@ -1,10 +1,79 @@
-- NOTE: This file is auto generated by ./sql-generator -- 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 -- MemoryRepository.getAssetIds
SELECT select
"memories_assets"."assetsId" AS "assetId" "assetsId"
FROM from
"memories_assets_assets" "memories_assets" "memories_assets_assets"
WHERE where
"memories_assets"."memoriesId" = $1 "memoriesId" = $1
AND "memories_assets"."assetsId" IN ($2) and "assetsId" in ($2)

View File

@ -1,49 +1,55 @@
import { Injectable } from '@nestjs/common'; 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 { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { MemoryEntity } from 'src/entities/memory.entity'; import { MemoryEntity } from 'src/entities/memory.entity';
import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { DataSource, In, Repository } from 'typeorm';
@Injectable() @Injectable()
export class MemoryRepository implements IMemoryRepository { export class MemoryRepository implements IMemoryRepository {
constructor( constructor(@InjectKysely() private db: Kysely<DB>) {}
@InjectRepository(MemoryEntity) private repository: Repository<MemoryEntity>,
@InjectDataSource() private dataSource: DataSource,
) {}
@GenerateSql({ params: [DummyValue.UUID] })
search(ownerId: string): Promise<MemoryEntity[]> { search(ownerId: string): Promise<MemoryEntity[]> {
return this.repository.find({ return this.db
where: { .selectFrom('memories')
ownerId, .selectAll()
}, .where('ownerId', '=', ownerId)
order: { .orderBy('memoryAt', 'desc')
memoryAt: 'DESC', .execute() as Promise<MemoryEntity[]>;
},
});
} }
@GenerateSql({ params: [DummyValue.UUID] })
get(id: string): Promise<MemoryEntity | null> { get(id: string): Promise<MemoryEntity | null> {
return this.repository.findOne({ return this.getByIdBuilder(id).executeTakeFirst() as unknown as Promise<MemoryEntity | null>;
where: { }
id,
}, async create(memory: Insertable<Memories>, assetIds: Set<string>): Promise<MemoryEntity> {
relations: { const id = await this.db.transaction().execute(async (tx) => {
assets: true, 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<MemoryEntity>;
} }
create(memory: Partial<MemoryEntity>): Promise<MemoryEntity> { @GenerateSql({ params: [DummyValue.UUID, { ownerId: DummyValue.UUID, isSaved: true }] })
return this.save(memory); async update(id: string, memory: Updateable<Memories>): Promise<MemoryEntity> {
} await this.db.updateTable('memories').set(memory).where('id', '=', id).execute();
return this.getByIdBuilder(id).executeTakeFirstOrThrow() as unknown as Promise<MemoryEntity>;
update(memory: Partial<MemoryEntity>): Promise<MemoryEntity> {
return this.save(memory);
} }
@GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.repository.delete({ id }); await this.db.deleteFrom('memories').where('id', '=', id).execute();
} }
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
@ -53,46 +59,49 @@ export class MemoryRepository implements IMemoryRepository {
return new Set(); return new Set();
} }
const results = await this.dataSource const results = await this.db
.createQueryBuilder() .selectFrom('memories_assets_assets')
.select('memories_assets.assetsId', 'assetId') .select(['assetsId'])
.from('memories_assets_assets', 'memories_assets') .where('memoriesId', '=', id)
.where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id }) .where('assetsId', 'in', assetIds)
.andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds }) .execute();
.getRawMany<{ assetId: string }>();
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<void> { async addAssetIds(id: string, assetIds: string[]): Promise<void> {
await this.dataSource await this.db
.createQueryBuilder() .insertInto('memories_assets_assets')
.insert()
.into('memories_assets_assets', ['memoriesId', 'assetsId'])
.values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId })))
.execute(); .execute();
} }
@Chunked({ paramIndex: 1 }) @Chunked({ paramIndex: 1 })
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
async removeAssetIds(id: string, assetIds: string[]): Promise<void> { async removeAssetIds(id: string, assetIds: string[]): Promise<void> {
await this.dataSource await this.db
.createQueryBuilder() .deleteFrom('memories_assets_assets')
.delete() .where('memoriesId', '=', id)
.from('memories_assets_assets') .where('assetsId', 'in', assetIds)
.where({
memoriesId: id,
assetsId: In(assetIds),
})
.execute(); .execute();
} }
private async save(memory: Partial<MemoryEntity>): Promise<MemoryEntity> { private getByIdBuilder(id: string) {
const { id } = await this.repository.save(memory); return this.db
return this.repository.findOneOrFail({ .selectFrom('memories')
where: { id }, .selectAll('memories')
relations: { .select((eb) =>
assets: true, 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);
} }
} }

View File

@ -69,7 +69,17 @@ describe(MemoryService.name, () => {
memoryAt: new Date(2024), memoryAt: new Date(2024),
}), }),
).resolves.toMatchObject({ assets: [] }); ).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 () => { it('should create a memory', async () => {
@ -80,14 +90,14 @@ describe(MemoryService.name, () => {
type: MemoryType.ON_THIS_DAY, type: MemoryType.ON_THIS_DAY,
data: { year: 2024 }, data: { year: 2024 },
assetIds: ['asset1'], assetIds: ['asset1'],
memoryAt: new Date(2024), memoryAt: new Date(2024, 0, 1),
}), }),
).resolves.toBeDefined(); ).resolves.toBeDefined();
expect(memoryMock.create).toHaveBeenCalledWith( expect(memoryMock.create).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
ownerId: userStub.admin.id, ownerId: userStub.admin.id,
assets: [{ id: 'asset1' }],
}), }),
new Set(['asset1']),
); );
}); });
@ -115,12 +125,7 @@ describe(MemoryService.name, () => {
accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
memoryMock.update.mockResolvedValue(memoryStub.memory1); memoryMock.update.mockResolvedValue(memoryStub.memory1);
await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined(); await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined();
expect(memoryMock.update).toHaveBeenCalledWith( expect(memoryMock.update).toHaveBeenCalledWith('memory1', expect.objectContaining({ isSaved: true }));
expect.objectContaining({
id: 'memory1',
isSaved: true,
}),
);
}); });
}); });

View File

@ -2,7 +2,6 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
@ -29,15 +28,17 @@ export class MemoryService extends BaseService {
permission: Permission.ASSET_SHARE, permission: Permission.ASSET_SHARE,
ids: assetIds, ids: assetIds,
}); });
const memory = await this.memoryRepository.create({ const memory = await this.memoryRepository.create(
ownerId: auth.user.id, {
type: dto.type, ownerId: auth.user.id,
data: dto.data, type: dto.type,
isSaved: dto.isSaved, data: dto.data,
memoryAt: dto.memoryAt, isSaved: dto.isSaved,
seenAt: dto.seenAt, memoryAt: dto.memoryAt,
assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity), seenAt: dto.seenAt,
}); },
allowedAssetIds,
);
return mapMemory(memory); return mapMemory(memory);
} }
@ -45,8 +46,7 @@ export class MemoryService extends BaseService {
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> { async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.MEMORY_UPDATE, ids: [id] });
const memory = await this.memoryRepository.update({ const memory = await this.memoryRepository.update(id, {
id,
isSaved: dto.isSaved, isSaved: dto.isSaved,
memoryAt: dto.memoryAt, memoryAt: dto.memoryAt,
seenAt: dto.seenAt, seenAt: dto.seenAt,
@ -68,7 +68,7 @@ export class MemoryService extends BaseService {
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) { if (hasSuccess) {
await this.memoryRepository.update({ id, updatedAt: new Date() }); await this.memoryRepository.update(id, { updatedAt: new Date() });
} }
return results; return results;
@ -86,7 +86,7 @@ export class MemoryService extends BaseService {
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) { if (hasSuccess) {
await this.memoryRepository.update({ id, updatedAt: new Date() }); await this.memoryRepository.update(id, { id, updatedAt: new Date() });
} }
return results; return results;