Skip to content

Commit

Permalink
refactor: migrate shared-link repository to kysely
Browse files Browse the repository at this point in the history
  • Loading branch information
danieldietzler committed Jan 12, 2025
1 parent cab2012 commit 7789963
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 400 deletions.
1 change: 1 addition & 0 deletions server/src/dtos/shared-link.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class SharedLinkResponseDto {
}

export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
console.log(sharedLink);
const linkAssets = sharedLink.assets || [];
const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset);

Expand Down
10 changes: 6 additions & 4 deletions server/src/interfaces/shared-link.interface.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Insertable } from 'kysely';
import { SharedLinks } from 'src/db';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';

export const ISharedLinkRepository = 'ISharedLinkRepository';

export interface ISharedLinkRepository {
getAll(userId: string): Promise<SharedLinkEntity[]>;
get(userId: string, id: string): Promise<SharedLinkEntity | null>;
getByKey(key: Buffer): Promise<SharedLinkEntity | null>;
create(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity>;
get(userId: string, id: string): Promise<SharedLinkEntity | undefined>;
getByKey(key: Buffer): Promise<SharedLinkEntity | undefined>;
create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity>;
update(entity: Partial<SharedLinkEntity> & { id: string }): Promise<SharedLinkEntity>;
remove(entity: SharedLinkEntity): Promise<void>;
}
470 changes: 147 additions & 323 deletions server/src/queries/shared.link.repository.sql

Large diffs are not rendered by default.

255 changes: 190 additions & 65 deletions server/src/repositories/shared-link.repository.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,215 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Insertable, Kysely, sql } from 'kysely';
import { jsonObjectFrom } from 'kysely/helpers/postgres';
import _ from 'lodash';
import { InjectKysely } from 'nestjs-kysely';
import { DB, SharedLinks } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { Repository } from 'typeorm';

@Injectable()
export class SharedLinkRepository implements ISharedLinkRepository {
constructor(@InjectRepository(SharedLinkEntity) private repository: Repository<SharedLinkEntity>) {}
constructor(@InjectKysely() private db: Kysely<DB>) {}

@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
get(userId: string, id: string): Promise<SharedLinkEntity | null> {
return this.repository.findOne({
where: {
id,
userId,
},
relations: {
assets: {
exifInfo: true,
},
album: {
assets: {
exifInfo: true,
},
owner: true,
},
},
order: {
createdAt: 'DESC',
assets: {
fileCreatedAt: 'ASC',
},
album: {
assets: {
fileCreatedAt: 'ASC',
},
},
},
});
get(userId: string, id: string): Promise<SharedLinkEntity | undefined> {
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
.leftJoinLateral(
(eb) =>
eb
.selectFrom('shared_link__asset')
.whereRef('shared_links.id', '=', 'shared_link__asset.sharedLinksId')
.innerJoin('assets', 'assets.id', 'shared_link__asset.assetsId')
.selectAll('assets')
.innerJoinLateral(
(eb) => eb.selectFrom('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
.as('a'),
(join) => join.onTrue(),
)
.leftJoinLateral(
(eb) =>
eb
.selectFrom('albums')
.selectAll('albums')
.whereRef('albums.id', '=', 'shared_links.albumId')
.innerJoin('albums_assets_assets', 'albums_assets_assets.albumsId', 'albums.id')
.innerJoinLateral(
(eb) =>
eb
.selectFrom('assets')
.selectAll('assets')
.whereRef('albums_assets_assets.assetsId', '=', 'assets.id')
.innerJoinLateral(
(eb) => eb.selectFrom('exif').whereRef('exif.assetId', '=', 'assets.id').as('exifInfo'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('exifInfo').as('exifInfo'))
.orderBy('a.fileCreatedAt', 'asc')
.as('assets'),
(join) => join.onTrue(),
)
.innerJoinLateral(
(eb) => eb.selectFrom('users').selectAll('users').whereRef('users.id', '=', 'a.ownerId').as('owner'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('assets').as('assets'))
.select((eb) => eb.fn.toJson('owner').as('owner'))
.as('album'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('a').as('assets'))
.select((eb) => eb.fn.toJson('album').as('album'))
.where('shared_links.id', '=', id)
.where('shared_links.userId', '=', userId)
.orderBy('shared_links.createdAt', 'desc')
.orderBy('a.fileCreatedAt', 'asc')
.executeTakeFirst() as Promise<SharedLinkEntity | undefined>;
}

@GenerateSql({ params: [DummyValue.UUID] })
getAll(userId: string): Promise<SharedLinkEntity[]> {
return this.repository.find({
where: {
userId,
},
relations: {
assets: true,
album: {
owner: true,
},
},
order: {
createdAt: 'DESC',
},
});
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
.where('shared_links.userId', '=', userId)
.leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id')
.leftJoinLateral(
(eb) =>
eb
.selectFrom('assets')
.whereRef('assets.id', '=', 'shared_link__asset.assetsId')
.selectAll('assets')
.as('assets'),
(join) => join.onTrue(),
)
.leftJoinLateral(
(eb) =>
eb
.selectFrom('albums')
.selectAll('albums')
.whereRef('albums.id', '=', 'shared_links.albumId')
.innerJoinLateral(
(eb) =>
eb
.selectFrom('users')
.select([
'users.id',
'users.email',
'users.createdAt',
'users.profileImagePath',
'users.isAdmin',
'users.shouldChangePassword',
'users.deletedAt',
'users.oauthId',
'users.updatedAt',
'users.storageLabel',
'users.name',
'users.quotaSizeInBytes',
'users.quotaUsageInBytes',
'users.status',
'users.profileChangedAt',
])
.whereRef('users.id', '=', 'albums.ownerId')
.as('owner'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('owner').as('owner'))
.as('album'),
(join) => join.onTrue(),
)
.select((eb) => eb.fn.toJson('album').as('album'))
.orderBy('album.createdAt', 'desc')
.execute() as unknown as Promise<SharedLinkEntity[]>;
}

@GenerateSql({ params: [DummyValue.BUFFER] })
async getByKey(key: Buffer): Promise<SharedLinkEntity | null> {
return await this.repository.findOne({
where: {
key,
},
relations: {
user: true,
},
});
async getByKey(key: Buffer): Promise<SharedLinkEntity | undefined> {
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
.where('shared_links.key', '=', key)
.select((eb) =>
jsonObjectFrom(
eb
.selectFrom('users')
.select([
'users.id',
'users.email',
'users.createdAt',
'users.profileImagePath',
'users.isAdmin',
'users.shouldChangePassword',
'users.deletedAt',
'users.oauthId',
'users.updatedAt',
'users.storageLabel',
'users.name',
'users.quotaSizeInBytes',
'users.quotaUsageInBytes',
'users.status',
'users.profileChangedAt',
])
.whereRef('users.id', '=', 'shared_links.userId'),
).as('user'),
)
.execute() as unknown as Promise<SharedLinkEntity | undefined>;
}

create(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
return this.save(entity);
}
async create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity> {
const { id } = await this.db
.insertInto('shared_links')
.values(_.omit(entity, 'assetIds'))
.returningAll()
.executeTakeFirstOrThrow();

if (entity.assetIds && entity.assetIds.length > 0) {
await this.db
.insertInto('shared_link__asset')
.values(entity.assetIds!.map((assetsId) => ({ assetsId, sharedLinksId: id })))
.execute();
}

update(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
return this.save(entity);
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
.where('shared_links.id', '=', id)
.leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id')
.leftJoinLateral(
(eb) =>
eb
.selectFrom('assets')
.whereRef('assets.id', '=', 'shared_link__asset.assetsId')
.selectAll('assets')
.innerJoinLateral(
(eb) => eb.selectFrom('exif').whereRef('exif.assetId', '=', 'assets.id').selectAll().as('exif'),
(join) => join.onTrue(),
)
.as('assets'),
(join) => join.onTrue(),
)
.select((eb) =>
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'),
)
.groupBy('shared_links.id')
.executeTakeFirstOrThrow() as unknown as Promise<SharedLinkEntity>;
}

async remove(entity: SharedLinkEntity): Promise<void> {
await this.repository.remove(entity);
update(entity: Partial<SharedLinkEntity> & { id: string }): Promise<SharedLinkEntity> {
return this.db
.updateTable('shared_links')
.set(entity)
.where('shared_links.id', '=', entity.id)
.returningAll()
.executeTakeFirstOrThrow() as Promise<SharedLinkEntity>;
}

private async save(entity: Partial<SharedLinkEntity>): Promise<SharedLinkEntity> {
await this.repository.save(entity);
return this.repository.findOneOrFail({ where: { id: entity.id } });
async remove(entity: SharedLinkEntity): Promise<void> {
await this.db.deleteFrom('shared_links').where('shared_links.id', '=', entity.id).execute();
}
}
1 change: 0 additions & 1 deletion server/src/services/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,6 @@ describe('AuthService', () => {

describe('validate - shared key', () => {
it('should not accept a non-existent key', async () => {
sharedLinkMock.getByKey.mockResolvedValue(null);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': 'key' },
Expand Down
8 changes: 2 additions & 6 deletions server/src/services/shared-link.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ describe(SharedLinkService.name, () => {

describe('get', () => {
it('should throw an error for an invalid shared link', async () => {
sharedLinkMock.get.mockResolvedValue(null);
await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(sharedLinkMock.update).not.toHaveBeenCalled();
Expand Down Expand Up @@ -130,7 +129,6 @@ describe(SharedLinkService.name, () => {
albumId: albumStub.oneAsset.id,
allowDownload: true,
allowUpload: true,
assets: [],
description: null,
expiresAt: null,
showExif: true,
Expand Down Expand Up @@ -160,7 +158,7 @@ describe(SharedLinkService.name, () => {
albumId: null,
allowDownload: true,
allowUpload: true,
assets: [{ id: assetStub.image.id }],
assetIds: [assetStub.image.id],
description: null,
expiresAt: null,
showExif: true,
Expand Down Expand Up @@ -190,7 +188,7 @@ describe(SharedLinkService.name, () => {
albumId: null,
allowDownload: false,
allowUpload: true,
assets: [{ id: assetStub.image.id }],
assetIds: [assetStub.image.id],
description: null,
expiresAt: null,
showExif: false,
Expand All @@ -201,7 +199,6 @@ describe(SharedLinkService.name, () => {

describe('update', () => {
it('should throw an error for an invalid shared link', async () => {
sharedLinkMock.get.mockResolvedValue(null);
await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(sharedLinkMock.update).not.toHaveBeenCalled();
Expand All @@ -222,7 +219,6 @@ describe(SharedLinkService.name, () => {

describe('remove', () => {
it('should throw an error for an invalid shared link', async () => {
sharedLinkMock.get.mockResolvedValue(null);
await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(sharedLinkMock.update).not.toHaveBeenCalled();
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/shared-link.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class SharedLinkService extends BaseService {
userId: auth.user.id,
type: dto.type,
albumId: dto.albumId || null,
assets: (dto.assetIds || []).map((id) => ({ id }) as AssetEntity),
assetIds: dto.assetIds,
description: dto.description || null,
password: dto.password,
expiresAt: dto.expiresAt || null,
Expand Down

0 comments on commit 7789963

Please sign in to comment.