Skip to content

Commit

Permalink
refactor: session repository (#15957)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 authored Feb 7, 2025
1 parent d7d4d22 commit 758449e
Show file tree
Hide file tree
Showing 11 changed files with 38 additions and 57 deletions.
4 changes: 2 additions & 2 deletions server/src/dtos/session.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SessionEntity } from 'src/entities/session.entity';
import { SessionItem } from 'src/types';

export class SessionResponseDto {
id!: string;
Expand All @@ -9,7 +9,7 @@ export class SessionResponseDto {
deviceOS!: string;
}

export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({
export const mapSession = (entity: SessionItem, currentId?: string): SessionResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
Expand Down
17 changes: 0 additions & 17 deletions server/src/interfaces/session.interface.ts

This file was deleted.

3 changes: 1 addition & 2 deletions server/src/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
Expand Down Expand Up @@ -72,6 +71,7 @@ export const repositories = [
NotificationRepository,
OAuthRepository,
ProcessRepository,
SessionRepository,
ServerInfoRepository,
SystemMetadataRepository,
TelemetryRepository,
Expand All @@ -93,7 +93,6 @@ export const providers = [
{ provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository },
{ provide: ISearchRepository, useClass: SearchRepository },
{ provide: ISessionRepository, useClass: SessionRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository },
Expand Down
35 changes: 15 additions & 20 deletions server/src/repositories/session.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,37 @@ import { Insertable, Kysely, Updateable } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB, Sessions } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { SessionEntity, withUser } from 'src/entities/session.entity';
import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface';
import { withUser } from 'src/entities/session.entity';
import { asUuid } from 'src/utils/database';

export type SessionSearchOptions = { updatedBefore: Date };

@Injectable()
export class SessionRepository implements ISessionRepository {
export class SessionRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}

@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
search(options: SessionSearchOptions): Promise<SessionEntity[]> {
search(options: SessionSearchOptions) {
return this.db
.selectFrom('sessions')
.selectAll()
.where('sessions.updatedAt', '<=', options.updatedBefore)
.execute() as Promise<SessionEntity[]>;
.execute();
}

@GenerateSql({ params: [DummyValue.STRING] })
getByToken(token: string): Promise<SessionEntity | undefined> {
getByToken(token: string) {
return this.db
.selectFrom('sessions')
.innerJoinLateral(withUser, (join) => join.onTrue())
.selectAll('sessions')
.select((eb) => eb.fn.toJson('user').as('user'))
.where('sessions.token', '=', token)
.executeTakeFirst() as Promise<SessionEntity | undefined>;
.executeTakeFirst();
}

@GenerateSql({ params: [DummyValue.UUID] })
getByUserId(userId: string): Promise<SessionEntity[]> {
getByUserId(userId: string) {
return this.db
.selectFrom('sessions')
.innerJoinLateral(withUser, (join) => join.onTrue())
Expand All @@ -41,30 +42,24 @@ export class SessionRepository implements ISessionRepository {
.where('sessions.userId', '=', userId)
.orderBy('sessions.updatedAt', 'desc')
.orderBy('sessions.createdAt', 'desc')
.execute() as unknown as Promise<SessionEntity[]>;
.execute();
}

async create(dto: Insertable<Sessions>): Promise<SessionEntity> {
const { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } = await this.db
.insertInto('sessions')
.values(dto)
.returningAll()
.executeTakeFirstOrThrow();

return { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } as SessionEntity;
create(dto: Insertable<Sessions>) {
return this.db.insertInto('sessions').values(dto).returningAll().executeTakeFirstOrThrow();
}

update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity> {
update(id: string, dto: Updateable<Sessions>) {
return this.db
.updateTable('sessions')
.set(dto)
.where('sessions.id', '=', asUuid(id))
.returningAll()
.executeTakeFirstOrThrow() as Promise<SessionEntity>;
.executeTakeFirstOrThrow();
}

@GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string): Promise<void> {
async delete(id: string) {
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
}
}
11 changes: 5 additions & 6 deletions server/src/services/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AuthService } from 'src/services/auth.service';
import { IApiKeyRepository, IOAuthRepository, ISystemMetadataRepository } from 'src/types';
import { IApiKeyRepository, IOAuthRepository, ISessionRepository, ISystemMetadataRepository } from 'src/types';
import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
Expand Down Expand Up @@ -257,7 +256,7 @@ describe('AuthService', () => {

it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(userStub.user1);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect(
sut.authenticate({
headers: { authorization: 'Bearer auth_token' },
Expand Down Expand Up @@ -362,7 +361,7 @@ describe('AuthService', () => {
});

it('should return an auth dto', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
Expand All @@ -376,7 +375,7 @@ describe('AuthService', () => {
});

it('should throw if admin route and not an admin', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
Expand All @@ -387,7 +386,7 @@ describe('AuthService', () => {
});

it('should update when access time exceeds an hour', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive as any);
sessionMock.update.mockResolvedValue(sessionStub.valid);
await expect(
sut.authenticate({
Expand Down
3 changes: 2 additions & 1 deletion server/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
mapLoginResponse,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { SessionEntity } from 'src/entities/session.entity';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
import { OAuthProfile } from 'src/repositories/oauth.repository';
Expand Down Expand Up @@ -338,7 +339,7 @@ export class AuthService extends BaseService {
await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
}

return { user: session.user, session };
return { user: session.user as unknown as UserEntity, session: session as unknown as SessionEntity };
}

throw new UnauthorizedException('Invalid user token');
Expand Down
4 changes: 2 additions & 2 deletions server/src/services/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
Expand All @@ -40,6 +39,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
Expand Down Expand Up @@ -80,7 +80,7 @@ export class BaseService {
protected processRepository: ProcessRepository,
@Inject(ISearchRepository) protected searchRepository: ISearchRepository,
protected serverInfoRepository: ServerInfoRepository,
@Inject(ISessionRepository) protected sessionRepository: ISessionRepository,
protected sessionRepository: SessionRepository,
@Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
@Inject(IStackRepository) protected stackRepository: IStackRepository,
@Inject(IStorageRepository) protected storageRepository: IStorageRepository,
Expand Down
8 changes: 3 additions & 5 deletions server/src/services/session.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { UserEntity } from 'src/entities/user.entity';
import { JobStatus } from 'src/interfaces/job.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { SessionService } from 'src/services/session.service';
import { ISessionRepository } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
Expand Down Expand Up @@ -38,7 +37,6 @@ describe('SessionService', () => {
deviceType: '',
id: '123',
token: '420',
user: {} as UserEntity,
userId: '42',
},
]);
Expand All @@ -50,7 +48,7 @@ describe('SessionService', () => {

describe('getAll', () => {
it('should get the devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid, sessionStub.inactive]);
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid as any, sessionStub.inactive]);
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
{
createdAt: '2021-01-01T00:00:00.000Z',
Expand All @@ -76,7 +74,7 @@ describe('SessionService', () => {

describe('logoutDevices', () => {
it('should logout all devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid]);
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]);

await sut.deleteAll(authStub.user1);

Expand Down
4 changes: 4 additions & 0 deletions server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
Expand Down Expand Up @@ -61,6 +62,7 @@ export type IMetricGroupRepository = RepositoryInterface<MetricGroupRepository>;
export type INotificationRepository = RepositoryInterface<NotificationRepository>;
export type IOAuthRepository = RepositoryInterface<OAuthRepository>;
export type IProcessRepository = RepositoryInterface<ProcessRepository>;
export type ISessionRepository = RepositoryInterface<SessionRepository>;
export type IServerInfoRepository = RepositoryInterface<ServerInfoRepository>;
export type ISystemMetadataRepository = RepositoryInterface<SystemMetadataRepository>;
export type ITelemetryRepository = RepositoryInterface<TelemetryRepository>;
Expand All @@ -81,6 +83,8 @@ export type MemoryItem =
| Awaited<ReturnType<IMemoryRepository['create']>>
| Awaited<ReturnType<IMemoryRepository['search']>>[0];

export type SessionItem = Awaited<ReturnType<ISessionRepository['getByUserId']>>[0];

export interface CropOptions {
top: number;
left: number;
Expand Down
2 changes: 1 addition & 1 deletion server/test/repositories/session.repository.mock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISessionRepository } from 'src/types';
import { Mocked, vitest } from 'vitest';

export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
Expand Down
4 changes: 3 additions & 1 deletion server/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
Expand All @@ -39,6 +40,7 @@ import {
IOAuthRepository,
IProcessRepository,
IServerInfoRepository,
ISessionRepository,
ISystemMetadataRepository,
ITrashRepository,
IVersionHistoryRepository,
Expand Down Expand Up @@ -170,7 +172,7 @@ export const newTestService = <T extends BaseService>(
processMock as IProcessRepository as ProcessRepository,
searchMock,
serverInfoMock as IServerInfoRepository as ServerInfoRepository,
sessionMock,
sessionMock as ISessionRepository as SessionRepository,
sharedLinkMock,
stackMock,
storageMock,
Expand Down

0 comments on commit 758449e

Please sign in to comment.