From 097183b31dc3dc4f9a6bf12e03fa2319a5f59be2 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 17 Jan 2025 18:49:21 -0500 Subject: [PATCH] refactor: migrate partner repo to kysely (#15366) --- server/src/interfaces/partner.interface.ts | 8 +- server/src/queries/partner.repository.sql | 189 ++++++++++++++++++ server/src/repositories/partner.repository.ts | 94 +++++++-- server/src/services/partner.service.spec.ts | 13 +- server/src/services/partner.service.ts | 2 +- 5 files changed, 276 insertions(+), 30 deletions(-) create mode 100644 server/src/queries/partner.repository.sql diff --git a/server/src/interfaces/partner.interface.ts b/server/src/interfaces/partner.interface.ts index 842745a51e..a6f50178ca 100644 --- a/server/src/interfaces/partner.interface.ts +++ b/server/src/interfaces/partner.interface.ts @@ -1,3 +1,5 @@ +import { Updateable } from 'kysely'; +import { Partners } from 'src/db'; import { PartnerEntity } from 'src/entities/partner.entity'; export interface PartnerIds { @@ -14,8 +16,8 @@ export const IPartnerRepository = 'IPartnerRepository'; export interface IPartnerRepository { getAll(userId: string): Promise; - get(partner: PartnerIds): Promise; + get(partner: PartnerIds): Promise; create(partner: PartnerIds): Promise; - remove(entity: PartnerEntity): Promise; - update(entity: Partial): Promise; + remove(partner: PartnerIds): Promise; + update(partner: PartnerIds, entity: Updateable): Promise; } diff --git a/server/src/queries/partner.repository.sql b/server/src/queries/partner.repository.sql new file mode 100644 index 0000000000..e115dc34b9 --- /dev/null +++ b/server/src/queries/partner.repository.sql @@ -0,0 +1,189 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- PartnerRepository.getAll +select + "partners".*, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" +from + "partners" + inner join "users" as "sharedBy" on "partners"."sharedById" = "sharedBy"."id" + and "sharedBy"."deletedAt" is null + inner join "users" as "sharedWith" on "partners"."sharedWithId" = "sharedWith"."id" + and "sharedWith"."deletedAt" is null +where + ( + "sharedWithId" = $1 + or "sharedById" = $2 + ) + +-- PartnerRepository.get +select + "partners".*, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" +from + "partners" + inner join "users" as "sharedBy" on "partners"."sharedById" = "sharedBy"."id" + and "sharedBy"."deletedAt" is null + inner join "users" as "sharedWith" on "partners"."sharedWithId" = "sharedWith"."id" + and "sharedWith"."deletedAt" is null +where + "sharedWithId" = $1 + and "sharedById" = $2 + +-- PartnerRepository.create +insert into + "partners" ("sharedWithId", "sharedById") +values + ($1, $2) +returning + *, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" + +-- PartnerRepository.update +update "partners" +set + "inTimeline" = $1 +where + "sharedWithId" = $2 + and "sharedById" = $3 +returning + *, + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedBy" + where + "sharedBy"."id" = "partners"."sharedById" + ) as obj + ) as "sharedBy", + ( + select + to_json(obj) + from + ( + select + "id", + "name", + "email", + "profileImagePath", + "profileChangedAt" + from + "users" as "sharedWith" + where + "sharedWith"."id" = "partners"."sharedWithId" + ) as obj + ) as "sharedWith" + +-- PartnerRepository.remove +delete from "partners" +where + "sharedWithId" = $1 + and "sharedById" = $2 diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts index 6b11a4e31e..929c06a1f5 100644 --- a/server/src/repositories/partner.repository.ts +++ b/server/src/repositories/partner.repository.ts @@ -1,37 +1,93 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { ExpressionBuilder, Insertable, JoinBuilder, Kysely, Updateable } from 'kysely'; +import { jsonObjectFrom } from 'kysely/helpers/postgres'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB, Partners, Users } from 'src/db'; +import { DummyValue, GenerateSql } from 'src/decorators'; import { PartnerEntity } from 'src/entities/partner.entity'; import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface'; -import { DeepPartial, Repository } from 'typeorm'; + +const columns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; + +const onSharedBy = (join: JoinBuilder) => + join.onRef('partners.sharedById', '=', 'sharedBy.id').on('sharedBy.deletedAt', 'is', null); + +const onSharedWith = (join: JoinBuilder) => + join.onRef('partners.sharedWithId', '=', 'sharedWith.id').on('sharedWith.deletedAt', 'is', null); + +const withSharedBy = (eb: ExpressionBuilder) => { + return jsonObjectFrom( + eb.selectFrom('users as sharedBy').select(columns).whereRef('sharedBy.id', '=', 'partners.sharedById'), + ).as('sharedBy'); +}; + +const withSharedWith = (eb: ExpressionBuilder) => { + return jsonObjectFrom( + eb.selectFrom('users as sharedWith').select(columns).whereRef('sharedWith.id', '=', 'partners.sharedWithId'), + ).as('sharedWith'); +}; @Injectable() export class PartnerRepository implements IPartnerRepository { - constructor(@InjectRepository(PartnerEntity) private repository: Repository) {} + constructor(@InjectKysely() private db: Kysely) {} + @GenerateSql({ params: [DummyValue.UUID] }) getAll(userId: string): Promise { - return this.repository.find({ where: [{ sharedWithId: userId }, { sharedById: userId }] }); + return this.db + .selectFrom('partners') + .innerJoin('users as sharedBy', onSharedBy) + .innerJoin('users as sharedWith', onSharedWith) + .selectAll('partners') + .select(withSharedBy) + .select(withSharedWith) + .where((eb) => eb.or([eb('sharedWithId', '=', userId), eb('sharedById', '=', userId)])) + .execute() as Promise; } - get({ sharedWithId, sharedById }: PartnerIds): Promise { - return this.repository.findOne({ where: { sharedById, sharedWithId } }); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) + get({ sharedWithId, sharedById }: PartnerIds): Promise { + return this.db + .selectFrom('partners') + .innerJoin('users as sharedBy', onSharedBy) + .innerJoin('users as sharedWith', onSharedWith) + .selectAll('partners') + .select(withSharedBy) + .select(withSharedWith) + .where('sharedWithId', '=', sharedWithId) + .where('sharedById', '=', sharedById) + .executeTakeFirst() as unknown as Promise; } - create({ sharedById, sharedWithId }: PartnerIds): Promise { - return this.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) + create(values: Insertable): Promise { + return this.db + .insertInto('partners') + .values(values) + .returningAll() + .returning(withSharedBy) + .returning(withSharedWith) + .executeTakeFirstOrThrow() as unknown as Promise; } - async remove(entity: PartnerEntity): Promise { - await this.repository.remove(entity); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }, { inTimeline: true }] }) + update({ sharedWithId, sharedById }: PartnerIds, values: Updateable): Promise { + return this.db + .updateTable('partners') + .set(values) + .where('sharedWithId', '=', sharedWithId) + .where('sharedById', '=', sharedById) + .returningAll() + .returning(withSharedBy) + .returning(withSharedWith) + .executeTakeFirstOrThrow() as unknown as Promise; } - update(entity: Partial): Promise { - return this.save(entity); - } - - private async save(entity: DeepPartial): Promise { - await this.repository.save(entity); - return this.repository.findOneOrFail({ - where: { sharedById: entity.sharedById, sharedWithId: entity.sharedWithId }, - }); + @GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] }) + async remove({ sharedWithId, sharedById }: PartnerIds): Promise { + await this.db + .deleteFrom('partners') + .where('sharedWithId', '=', sharedWithId) + .where('sharedById', '=', sharedById) + .execute(); } } diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index 2e11c4f9ad..e7b7348e98 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -37,7 +37,7 @@ describe(PartnerService.name, () => { describe('create', () => { it('should create a new partner', async () => { - partnerMock.get.mockResolvedValue(null); + partnerMock.get.mockResolvedValue(void 0); partnerMock.create.mockResolvedValue(partnerStub.adminToUser1); await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toBeDefined(); @@ -67,7 +67,7 @@ describe(PartnerService.name, () => { }); it('should throw an error when the partner does not exist', async () => { - partnerMock.get.mockResolvedValue(null); + partnerMock.get.mockResolvedValue(void 0); await expect(sut.remove(authStub.admin, authStub.user1.user.id)).rejects.toBeInstanceOf(BadRequestException); @@ -87,11 +87,10 @@ describe(PartnerService.name, () => { partnerMock.update.mockResolvedValue(partnerStub.adminToUser1); await expect(sut.update(authStub.admin, 'shared-by-id', { inTimeline: true })).resolves.toBeDefined(); - expect(partnerMock.update).toHaveBeenCalledWith({ - sharedById: 'shared-by-id', - sharedWithId: authStub.admin.user.id, - inTimeline: true, - }); + expect(partnerMock.update).toHaveBeenCalledWith( + { sharedById: 'shared-by-id', sharedWithId: authStub.admin.user.id }, + { inTimeline: true }, + ); }); }); }); diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index ee36f1ce45..f17bab24ba 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -43,7 +43,7 @@ export class PartnerService extends BaseService { await this.requireAccess({ auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; - const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline }); + const entity = await this.partnerRepository.update(partnerId, { inTimeline: dto.inTimeline }); return this.mapPartner(entity, PartnerDirection.SharedWith); }